Ecoer Logo

@marnee

40

Programmer and ham radio enthusiast

steemit.com/@marnee
VOTING POWER100.00%
DOWNVOTE POWER100.00%
RESOURCE CREDITS100.00%
REPUTATION PROGRESS89.98%
Net Worth
2.042USD
STEEM
0.001STEEM
SBD
4.069SBD
Effective Power
5.001SP
├── Own SP
0.634SP
└── Incoming Deleg
+4.367SP

Detailed Balance

STEEM
balance
0.001STEEM
market_balance
0.000STEEM
savings_balance
0.000STEEM
reward_steem_balance
0.000STEEM
STEEM POWER
Own SP
0.634SP
Delegated Out
0.000SP
Delegation In
4.367SP
Effective Power
5.001SP
Reward SP (pending)
2.779SP
SBD
sbd_balance
0.000SBD
sbd_conversions
0.000SBD
sbd_market_balance
0.000SBD
savings_sbd_balance
0.000SBD
reward_sbd_balance
4.069SBD
{
  "balance": "0.001 STEEM",
  "savings_balance": "0.000 STEEM",
  "reward_steem_balance": "0.000 STEEM",
  "vesting_shares": "1031.889322 VESTS",
  "delegated_vesting_shares": "0.000000 VESTS",
  "received_vesting_shares": "7111.770484 VESTS",
  "sbd_balance": "0.000 SBD",
  "savings_sbd_balance": "0.000 SBD",
  "reward_sbd_balance": "4.069 SBD",
  "conversions": []
}

Account Info

namemarnee
id314875
rank1,428,054
reputation45241130999
created2017-08-16T03:52:03
recovery_accountsteem
proxyNone
post_count18
comment_count0
lifetime_vote_count0
witnesses_voted_for0
last_post2019-05-19T19:31:36
last_root_post2019-05-19T19:31:36
last_vote_time2019-03-31T00:58:45
proxied_vsf_votes0, 0, 0, 0
can_vote1
voting_power0
delayed_votes0
balance0.001 STEEM
savings_balance0.000 STEEM
sbd_balance0.000 SBD
savings_sbd_balance0.000 SBD
vesting_shares1031.889322 VESTS
delegated_vesting_shares0.000000 VESTS
received_vesting_shares7111.770484 VESTS
reward_vesting_balance5719.196642 VESTS
vesting_balance0.000 STEEM
vesting_withdraw_rate0.000000 VESTS
next_vesting_withdrawal1969-12-31T23:59:59
withdrawn0
to_withdraw0
withdraw_routes0
savings_withdraw_requests0
last_account_recovery1970-01-01T00:00:00
reset_accountnull
last_owner_update1970-01-01T00:00:00
last_account_update2017-12-16T18:02:54
minedNo
sbd_seconds0
sbd_last_interest_payment1970-01-01T00:00:00
savings_sbd_last_interest_payment1970-01-01T00:00:00
{
  "id": 314875,
  "name": "marnee",
  "owner": {
    "weight_threshold": 1,
    "account_auths": [],
    "key_auths": [
      [
        "STM5Nj1zUJJ9ySh9kBF2Movdvrp64h64gvd3sX5yDCi2vyXdV3qA8",
        1
      ]
    ]
  },
  "active": {
    "weight_threshold": 1,
    "account_auths": [],
    "key_auths": [
      [
        "STM6Lusj37LXmBTrYSZzwSPKnRuyKPUDShQTLKCBLGCPCaEU1of1w",
        1
      ]
    ]
  },
  "posting": {
    "weight_threshold": 1,
    "account_auths": [],
    "key_auths": [
      [
        "STM5BheYsZWVkffyGjbywqhPqgMfiDnACLGknd3sCKwpbDuwYgWqG",
        1
      ]
    ]
  },
  "memo_key": "STM6aQ2Kzb3qwPfPiy434uLdAcfT9RxVZs22Lo2pDsHjidYMWMXNH",
  "json_metadata": "{\"profile\":{\"name\":\"Marnee Dearman\",\"about\":\"Programmer and ham radio enthusiast\",\"profile_image\":\"https://postimg.org/imahttps://s26.postimg.org/v26zso1d5/Couch_Profile.jpgge/44d2qxgpx/\"}}",
  "posting_json_metadata": "{\"profile\":{\"name\":\"Marnee Dearman\",\"about\":\"Programmer and ham radio enthusiast\",\"profile_image\":\"https://postimg.org/imahttps://s26.postimg.org/v26zso1d5/Couch_Profile.jpgge/44d2qxgpx/\"}}",
  "proxy": "",
  "last_owner_update": "1970-01-01T00:00:00",
  "last_account_update": "2017-12-16T18:02:54",
  "created": "2017-08-16T03:52:03",
  "mined": false,
  "recovery_account": "steem",
  "last_account_recovery": "1970-01-01T00:00:00",
  "reset_account": "null",
  "comment_count": 0,
  "lifetime_vote_count": 0,
  "post_count": 18,
  "can_vote": true,
  "voting_manabar": {
    "current_mana": "8143659806",
    "last_update_time": 1779074847
  },
  "downvote_manabar": {
    "current_mana": 2035914951,
    "last_update_time": 1779074847
  },
  "voting_power": 0,
  "balance": "0.001 STEEM",
  "savings_balance": "0.000 STEEM",
  "sbd_balance": "0.000 SBD",
  "sbd_seconds": "0",
  "sbd_seconds_last_update": "1970-01-01T00:00:00",
  "sbd_last_interest_payment": "1970-01-01T00:00:00",
  "savings_sbd_balance": "0.000 SBD",
  "savings_sbd_seconds": "0",
  "savings_sbd_seconds_last_update": "1970-01-01T00:00:00",
  "savings_sbd_last_interest_payment": "1970-01-01T00:00:00",
  "savings_withdraw_requests": 0,
  "reward_sbd_balance": "4.069 SBD",
  "reward_steem_balance": "0.000 STEEM",
  "reward_vesting_balance": "5719.196642 VESTS",
  "reward_vesting_steem": "2.779 STEEM",
  "vesting_shares": "1031.889322 VESTS",
  "delegated_vesting_shares": "0.000000 VESTS",
  "received_vesting_shares": "7111.770484 VESTS",
  "vesting_withdraw_rate": "0.000000 VESTS",
  "next_vesting_withdrawal": "1969-12-31T23:59:59",
  "withdrawn": 0,
  "to_withdraw": 0,
  "withdraw_routes": 0,
  "curation_rewards": 4,
  "posting_rewards": 5546,
  "proxied_vsf_votes": [
    0,
    0,
    0,
    0
  ],
  "witnesses_voted_for": 0,
  "last_post": "2019-05-19T19:31:36",
  "last_root_post": "2019-05-19T19:31:36",
  "last_vote_time": "2019-03-31T00:58:45",
  "post_bandwidth": 0,
  "pending_claimed_accounts": 0,
  "vesting_balance": "0.000 STEEM",
  "reputation": "45241130999",
  "transfer_history": [],
  "market_history": [],
  "post_history": [],
  "vote_history": [],
  "other_history": [],
  "witness_votes": [],
  "tags_usage": [],
  "guest_bloggers": [],
  "rank": 1428054
}

Withdraw Routes

IncomingOutgoing
Empty
Empty
{
  "incoming": [],
  "outgoing": []
}
From Date
To Date
steemdelegated 4.367 SP to @marnee
2026/05/18 03:27:27
delegatorsteem
delegateemarnee
vesting shares7111.770484 VESTS
Transaction InfoBlock #106147274/Trx 3fac7c98ae1c3a4f4ebc2d56c4dc48b53f34eb53
View Raw JSON Data
{
  "trx_id": "3fac7c98ae1c3a4f4ebc2d56c4dc48b53f34eb53",
  "block": 106147274,
  "trx_in_block": 0,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2026-05-18T03:27:27",
  "op": [
    "delegate_vesting_shares",
    {
      "delegator": "steem",
      "delegatee": "marnee",
      "vesting_shares": "7111.770484 VESTS"
    }
  ]
}
steemdelegated 2.702 SP to @marnee
2026/05/12 16:50:09
delegatorsteem
delegateemarnee
vesting shares4399.560079 VESTS
Transaction InfoBlock #105991273/Trx eb2810e855567595caf033ae8933f6d63f60429f
View Raw JSON Data
{
  "trx_id": "eb2810e855567595caf033ae8933f6d63f60429f",
  "block": 105991273,
  "trx_in_block": 8,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2026-05-12T16:50:09",
  "op": [
    "delegate_vesting_shares",
    {
      "delegator": "steem",
      "delegatee": "marnee",
      "vesting_shares": "4399.560079 VESTS"
    }
  ]
}
steemdelegated 4.375 SP to @marnee
2026/04/26 02:43:27
delegatorsteem
delegateemarnee
vesting shares7124.286240 VESTS
Transaction InfoBlock #105514837/Trx 2d44ec3bb6e52369de23fcf8c1e219439311edf6
View Raw JSON Data
{
  "trx_id": "2d44ec3bb6e52369de23fcf8c1e219439311edf6",
  "block": 105514837,
  "trx_in_block": 0,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2026-04-26T02:43:27",
  "op": [
    "delegate_vesting_shares",
    {
      "delegator": "steem",
      "delegatee": "marnee",
      "vesting_shares": "7124.286240 VESTS"
    }
  ]
}
steemdelegated 2.727 SP to @marnee
2026/01/23 16:23:45
delegatorsteem
delegateemarnee
vesting shares4441.106898 VESTS
Transaction InfoBlock #102862108/Trx 2d90319313ed9d56b093ad1112bad383096ffcdd
View Raw JSON Data
{
  "trx_id": "2d90319313ed9d56b093ad1112bad383096ffcdd",
  "block": 102862108,
  "trx_in_block": 0,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2026-01-23T16:23:45",
  "op": [
    "delegate_vesting_shares",
    {
      "delegator": "steem",
      "delegatee": "marnee",
      "vesting_shares": "4441.106898 VESTS"
    }
  ]
}
steemdelegated 2.828 SP to @marnee
2024/12/17 11:37:03
delegatorsteem
delegateemarnee
vesting shares4605.326095 VESTS
Transaction InfoBlock #91308388/Trx dba6fba7a6c7ff4e4502617fb7b010910bf8ed92
View Raw JSON Data
{
  "trx_id": "dba6fba7a6c7ff4e4502617fb7b010910bf8ed92",
  "block": 91308388,
  "trx_in_block": 0,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2024-12-17T11:37:03",
  "op": [
    "delegate_vesting_shares",
    {
      "delegator": "steem",
      "delegatee": "marnee",
      "vesting_shares": "4605.326095 VESTS"
    }
  ]
}
steemdelegated 2.932 SP to @marnee
2023/11/14 03:19:06
delegatorsteem
delegateemarnee
vesting shares4774.459627 VESTS
Transaction InfoBlock #79862567/Trx 5a11d6b6fbcbfb600f1671b908d63aa099b197eb
View Raw JSON Data
{
  "trx_id": "5a11d6b6fbcbfb600f1671b908d63aa099b197eb",
  "block": 79862567,
  "trx_in_block": 3,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2023-11-14T03:19:06",
  "op": [
    "delegate_vesting_shares",
    {
      "delegator": "steem",
      "delegatee": "marnee",
      "vesting_shares": "4774.459627 VESTS"
    }
  ]
}
steemdelegated 4.736 SP to @marnee
2023/09/22 01:35:48
delegatorsteem
delegateemarnee
vesting shares7711.738413 VESTS
Transaction InfoBlock #78352338/Trx af8e2d367e07ad43d82c7822bd2f54442747d60e
View Raw JSON Data
{
  "trx_id": "af8e2d367e07ad43d82c7822bd2f54442747d60e",
  "block": 78352338,
  "trx_in_block": 0,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2023-09-22T01:35:48",
  "op": [
    "delegate_vesting_shares",
    {
      "delegator": "steem",
      "delegatee": "marnee",
      "vesting_shares": "7711.738413 VESTS"
    }
  ]
}
steemdelegated 4.872 SP to @marnee
2022/11/03 14:55:42
delegatorsteem
delegateemarnee
vesting shares7933.419851 VESTS
Transaction InfoBlock #69117110/Trx aaea75a6fd51bcbabc45ebae4d9d436cdb68f478
View Raw JSON Data
{
  "trx_id": "aaea75a6fd51bcbabc45ebae4d9d436cdb68f478",
  "block": 69117110,
  "trx_in_block": 1,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2022-11-03T14:55:42",
  "op": [
    "delegate_vesting_shares",
    {
      "delegator": "steem",
      "delegatee": "marnee",
      "vesting_shares": "7933.419851 VESTS"
    }
  ]
}
steemdelegated 5.007 SP to @marnee
2022/01/17 18:11:03
delegatorsteem
delegateemarnee
vesting shares8153.654987 VESTS
Transaction InfoBlock #60818044/Trx caa98b5cdc18de4b3ed3a1648ea7a101016a0039
View Raw JSON Data
{
  "trx_id": "caa98b5cdc18de4b3ed3a1648ea7a101016a0039",
  "block": 60818044,
  "trx_in_block": 21,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2022-01-17T18:11:03",
  "op": [
    "delegate_vesting_shares",
    {
      "delegator": "steem",
      "delegatee": "marnee",
      "vesting_shares": "8153.654987 VESTS"
    }
  ]
}
steemdelegated 5.120 SP to @marnee
2021/06/14 03:42:18
delegatorsteem
delegateemarnee
vesting shares8337.721740 VESTS
Transaction InfoBlock #54611172/Trx 09ddfbe4d09098caca345e2f8e3ded844d435201
View Raw JSON Data
{
  "trx_id": "09ddfbe4d09098caca345e2f8e3ded844d435201",
  "block": 54611172,
  "trx_in_block": 4,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2021-06-14T03:42:18",
  "op": [
    "delegate_vesting_shares",
    {
      "delegator": "steem",
      "delegatee": "marnee",
      "vesting_shares": "8337.721740 VESTS"
    }
  ]
}
steemdelegated 5.235 SP to @marnee
2020/12/11 13:57:33
delegatorsteem
delegateemarnee
vesting shares8525.143714 VESTS
Transaction InfoBlock #49358521/Trx f3ee46256f60453b9fd07493d9374c54723c79fb
View Raw JSON Data
{
  "trx_id": "f3ee46256f60453b9fd07493d9374c54723c79fb",
  "block": 49358521,
  "trx_in_block": 7,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2020-12-11T13:57:33",
  "op": [
    "delegate_vesting_shares",
    {
      "delegator": "steem",
      "delegatee": "marnee",
      "vesting_shares": "8525.143714 VESTS"
    }
  ]
}
steemdelegated 1.175 SP to @marnee
2020/12/06 07:33:48
delegatorsteem
delegateemarnee
vesting shares1912.543513 VESTS
Transaction InfoBlock #49210061/Trx af5bf5f0590211d2a50db1eacb22a3b0769863d0
View Raw JSON Data
{
  "trx_id": "af5bf5f0590211d2a50db1eacb22a3b0769863d0",
  "block": 49210061,
  "trx_in_block": 0,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2020-12-06T07:33:48",
  "op": [
    "delegate_vesting_shares",
    {
      "delegator": "steem",
      "delegatee": "marnee",
      "vesting_shares": "1912.543513 VESTS"
    }
  ]
}
steemdelegated 5.239 SP to @marnee
2020/12/05 17:35:24
delegatorsteem
delegateemarnee
vesting shares8531.351568 VESTS
Transaction InfoBlock #49193608/Trx 4a455dddb77aeaf9f833393d571100a4c7f1ffcd
View Raw JSON Data
{
  "trx_id": "4a455dddb77aeaf9f833393d571100a4c7f1ffcd",
  "block": 49193608,
  "trx_in_block": 7,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2020-12-05T17:35:24",
  "op": [
    "delegate_vesting_shares",
    {
      "delegator": "steem",
      "delegatee": "marnee",
      "vesting_shares": "8531.351568 VESTS"
    }
  ]
}
steemdelegated 1.179 SP to @marnee
2020/11/02 21:28:36
delegatorsteem
delegateemarnee
vesting shares1920.017158 VESTS
Transaction InfoBlock #48264678/Trx bf57152525bdaa46752da2e1ad2d55cc27c19974
View Raw JSON Data
{
  "trx_id": "bf57152525bdaa46752da2e1ad2d55cc27c19974",
  "block": 48264678,
  "trx_in_block": 1,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2020-11-02T21:28:36",
  "op": [
    "delegate_vesting_shares",
    {
      "delegator": "steem",
      "delegatee": "marnee",
      "vesting_shares": "1920.017158 VESTS"
    }
  ]
}
steemdelegated 5.364 SP to @marnee
2020/05/09 08:34:18
delegatorsteem
delegateemarnee
vesting shares8734.156927 VESTS
Transaction InfoBlock #43220350/Trx 2bda45a4b7f147780b9ef9e908a84e67701769cc
View Raw JSON Data
{
  "trx_id": "2bda45a4b7f147780b9ef9e908a84e67701769cc",
  "block": 43220350,
  "trx_in_block": 4,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2020-05-09T08:34:18",
  "op": [
    "delegate_vesting_shares",
    {
      "delegator": "steem",
      "delegatee": "marnee",
      "vesting_shares": "8734.156927 VESTS"
    }
  ]
}
steemdelegated 1.200 SP to @marnee
2020/05/08 12:36:18
delegatorsteem
delegateemarnee
vesting shares1953.311140 VESTS
Transaction InfoBlock #43196959/Trx d802c10f8661ef4e77dc44aded35260c0362e6a9
View Raw JSON Data
{
  "trx_id": "d802c10f8661ef4e77dc44aded35260c0362e6a9",
  "block": 43196959,
  "trx_in_block": 3,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2020-05-08T12:36:18",
  "op": [
    "delegate_vesting_shares",
    {
      "delegator": "steem",
      "delegatee": "marnee",
      "vesting_shares": "1953.311140 VESTS"
    }
  ]
}
dtubesent 0.001 STEEM to @marnee- "Time is running out, claim your DTube account now before anyone else can! Login at https://d.tube"
2019/08/22 17:20:45
fromdtube
tomarnee
amount0.001 STEEM
memoTime is running out, claim your DTube account now before anyone else can! Login at https://d.tube
Transaction InfoBlock #35780823/Trx 696686cf36420ff57631529048331dce1f202a5e
View Raw JSON Data
{
  "trx_id": "696686cf36420ff57631529048331dce1f202a5e",
  "block": 35780823,
  "trx_in_block": 0,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2019-08-22T17:20:45",
  "op": [
    "transfer",
    {
      "from": "dtube",
      "to": "marnee",
      "amount": "0.001 STEEM",
      "memo": "Time is running out, claim your DTube account now before anyone else can! Login at https://d.tube"
    }
  ]
}
steemdelegated 5.456 SP to @marnee
2019/08/18 20:39:54
delegatorsteem
delegateemarnee
vesting shares8884.734874 VESTS
Transaction InfoBlock #35669782/Trx ed9ffd57b4ec12d2fdbff31e5a0965548a407a10
View Raw JSON Data
{
  "trx_id": "ed9ffd57b4ec12d2fdbff31e5a0965548a407a10",
  "block": 35669782,
  "trx_in_block": 8,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2019-08-18T20:39:54",
  "op": [
    "delegate_vesting_shares",
    {
      "delegator": "steem",
      "delegatee": "marnee",
      "vesting_shares": "8884.734874 VESTS"
    }
  ]
}
2019/08/16 04:25:09
parent authormarnee
parent permlinkf-for-aprs
authorsteemitboard
permlinksteemitboard-notify-marnee-20190816t042508000z
title
bodyCongratulations @marnee! You received a personal award! <table><tr><td>https://steemitimages.com/70x70/http://steemitboard.com/@marnee/birthday2.png</td><td>Happy Birthday! - You are on the Steem blockchain for 2 years!</td></tr></table> <sub>_You can view [your badges on your Steem Board](https://steemitboard.com/@marnee) and compare to others on the [Steem Ranking](https://steemitboard.com/ranking/index.php?name=marnee)_</sub> ###### [Vote for @Steemitboard as a witness](https://v2.steemconnect.com/sign/account-witness-vote?witness=steemitboard&approve=1) to get one more award and increased upvotes!
json metadata{"image":["https://steemitboard.com/img/notify.png"]}
Transaction InfoBlock #35592838/Trx 0322c7d3f0683cf722602e4a983ea999c51811a7
View Raw JSON Data
{
  "trx_id": "0322c7d3f0683cf722602e4a983ea999c51811a7",
  "block": 35592838,
  "trx_in_block": 4,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2019-08-16T04:25:09",
  "op": [
    "comment",
    {
      "parent_author": "marnee",
      "parent_permlink": "f-for-aprs",
      "author": "steemitboard",
      "permlink": "steemitboard-notify-marnee-20190816t042508000z",
      "title": "",
      "body": "Congratulations @marnee! You received a personal award!\n\n<table><tr><td>https://steemitimages.com/70x70/http://steemitboard.com/@marnee/birthday2.png</td><td>Happy Birthday! - You are on the Steem blockchain for 2 years!</td></tr></table>\n\n<sub>_You can view [your badges on your Steem Board](https://steemitboard.com/@marnee) and compare to others on the [Steem Ranking](https://steemitboard.com/ranking/index.php?name=marnee)_</sub>\n\n\n###### [Vote for @Steemitboard as a witness](https://v2.steemconnect.com/sign/account-witness-vote?witness=steemitboard&approve=1) to get one more award and increased upvotes!",
      "json_metadata": "{\"image\":[\"https://steemitboard.com/img/notify.png\"]}"
    }
  ]
}
steemdelegated 17.666 SP to @marnee
2019/07/22 19:05:33
delegatorsteem
delegateemarnee
vesting shares28767.250819 VESTS
Transaction InfoBlock #34892864/Trx c845d9e9df52bdb45e1b7c7b16d8fbbd4db53c0f
View Raw JSON Data
{
  "trx_id": "c845d9e9df52bdb45e1b7c7b16d8fbbd4db53c0f",
  "block": 34892864,
  "trx_in_block": 15,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2019-07-22T19:05:33",
  "op": [
    "delegate_vesting_shares",
    {
      "delegator": "steem",
      "delegatee": "marnee",
      "vesting_shares": "28767.250819 VESTS"
    }
  ]
}
lion200upvoted (100.00%) @marnee / f-for-aprs
2019/05/20 00:59:48
voterlion200
authormarnee
permlinkf-for-aprs
weight10000 (100.00%)
Transaction InfoBlock #33059065/Trx a16c9a613e52df3211a92e33daadbac63e2402af
View Raw JSON Data
{
  "trx_id": "a16c9a613e52df3211a92e33daadbac63e2402af",
  "block": 33059065,
  "trx_in_block": 9,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2019-05-20T00:59:48",
  "op": [
    "vote",
    {
      "voter": "lion200",
      "author": "marnee",
      "permlink": "f-for-aprs",
      "weight": 10000
    }
  ]
}
marneepublished a new post: f-for-aprs
2019/05/19 19:33:18
parent author
parent permlinkfsharp
authormarnee
permlinkf-for-aprs
titleF# for APRS
body@@ -1,8 +1,111 @@ +!%5Balt text%5D%5Blogo%5D%0A%0A%5Blogo%5D: https://raw.githubusercontent.com/MarneeDear/FAPRS/master/logo.png %22FAPRS%22%0A%0A A system
json metadata{"tags":["fsharp","radio","aprs"],"image":["https://raw.githubusercontent.com/MarneeDear/FAPRS/master/logo.png","https://img.youtube.com/vi/8x6x_6mDVlQ/0.jpg","https://img.youtube.com/vi/FJEVWMuz6Xg/0.jpg","https://i.imgur.com/9w47hfD.png","https://i.imgur.com/QoR9wYK.gif","https://i.imgur.com/ksav8IY.png","https://img.youtube.com/vi/OgFBXfwmKYc/0.jpg"],"links":["http://foundation.fsharp.org/applied_fsharp_challenge","https://github.com/MarneeDear/FAPRS","https://fsharpforfunandprofit.com/posts/designing-with-types-intro/","https://fsharpforfunandprofit.com/posts/convenience-active-patterns/","https://fsharpforfunandprofit.com/posts/recipe-part2/","http://fsprojects.github.io/Argu/","https://github.com/haf/expecto","https://saturnframework.org/","https://youtu.be/8x6x_6mDVlQ","http://www.youtube.com/watch?v=FJEVWMuz6Xg","http://www.nwclimate.org/aprs/digipeater/","https://www.raspberrypi.org/products/raspberry-pi-3-model-b/","https://baofengtech.com/uv82","https://baofengtech.com/aprs-k2-trrs-cable","https://www.tapr.org/aprs_information.html","http://www.aprs.org/doc/APRS101.PDF","http://www.aprs.org/aprs11.html","http://www.aprs.org/aprs12.html","https://fsharpforfunandprofit.com/posts/designing-with-types-single-case-dus/","http://aprs.org/aprs11/tocalls.txt","http://wa8lmf.net/DigiPaths/","https://ham.stackexchange.com/questions/6213/help-understanding-path-taken-by-aprs-packet?rq=1","https://en.wikipedia.org/wiki/AX.25","https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/active-patterns","https://github.com/MarneeDear/FAPRS/blob/master/README.md","https://aprs.fi","http://www.aprs.org/","https://www.youtube.com/watch?v=OgFBXfwmKYc","https://github.com/ampledata/aprs","https://aprsdroid.org/"],"app":"steemit/0.1","format":"markdown"}
Transaction InfoBlock #33052536/Trx d4b4296929a7090ecd2eed33c782d3061624d550
View Raw JSON Data
{
  "trx_id": "d4b4296929a7090ecd2eed33c782d3061624d550",
  "block": 33052536,
  "trx_in_block": 28,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2019-05-19T19:33:18",
  "op": [
    "comment",
    {
      "parent_author": "",
      "parent_permlink": "fsharp",
      "author": "marnee",
      "permlink": "f-for-aprs",
      "title": "F# for APRS",
      "body": "@@ -1,8 +1,111 @@\n+!%5Balt text%5D%5Blogo%5D%0A%0A%5Blogo%5D: https://raw.githubusercontent.com/MarneeDear/FAPRS/master/logo.png %22FAPRS%22%0A%0A\n A system\n",
      "json_metadata": "{\"tags\":[\"fsharp\",\"radio\",\"aprs\"],\"image\":[\"https://raw.githubusercontent.com/MarneeDear/FAPRS/master/logo.png\",\"https://img.youtube.com/vi/8x6x_6mDVlQ/0.jpg\",\"https://img.youtube.com/vi/FJEVWMuz6Xg/0.jpg\",\"https://i.imgur.com/9w47hfD.png\",\"https://i.imgur.com/QoR9wYK.gif\",\"https://i.imgur.com/ksav8IY.png\",\"https://img.youtube.com/vi/OgFBXfwmKYc/0.jpg\"],\"links\":[\"http://foundation.fsharp.org/applied_fsharp_challenge\",\"https://github.com/MarneeDear/FAPRS\",\"https://fsharpforfunandprofit.com/posts/designing-with-types-intro/\",\"https://fsharpforfunandprofit.com/posts/convenience-active-patterns/\",\"https://fsharpforfunandprofit.com/posts/recipe-part2/\",\"http://fsprojects.github.io/Argu/\",\"https://github.com/haf/expecto\",\"https://saturnframework.org/\",\"https://youtu.be/8x6x_6mDVlQ\",\"http://www.youtube.com/watch?v=FJEVWMuz6Xg\",\"http://www.nwclimate.org/aprs/digipeater/\",\"https://www.raspberrypi.org/products/raspberry-pi-3-model-b/\",\"https://baofengtech.com/uv82\",\"https://baofengtech.com/aprs-k2-trrs-cable\",\"https://www.tapr.org/aprs_information.html\",\"http://www.aprs.org/doc/APRS101.PDF\",\"http://www.aprs.org/aprs11.html\",\"http://www.aprs.org/aprs12.html\",\"https://fsharpforfunandprofit.com/posts/designing-with-types-single-case-dus/\",\"http://aprs.org/aprs11/tocalls.txt\",\"http://wa8lmf.net/DigiPaths/\",\"https://ham.stackexchange.com/questions/6213/help-understanding-path-taken-by-aprs-packet?rq=1\",\"https://en.wikipedia.org/wiki/AX.25\",\"https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/active-patterns\",\"https://github.com/MarneeDear/FAPRS/blob/master/README.md\",\"https://aprs.fi\",\"http://www.aprs.org/\",\"https://www.youtube.com/watch?v=OgFBXfwmKYc\",\"https://github.com/ampledata/aprs\",\"https://aprsdroid.org/\"],\"app\":\"steemit/0.1\",\"format\":\"markdown\"}"
    }
  ]
}
marneepublished a new post: f-for-aprs
2019/05/19 19:31:36
parent author
parent permlinkfsharp
authormarnee
permlinkf-for-aprs
titleF# for APRS
bodyA system for sending and receiving APRS messages integrated with DireWolf, and built on .NET Core in F#. _This is also my submission for the [Applied F# Challenge](http://foundation.fsharp.org/applied_fsharp_challenge) - F# in your organization or domain category._ ## Applied F# Challenge #### Author Marnee Dearman (KG7SIO) #### Github Repository [MarneeDear/FAPRS](https://github.com/MarneeDear/FAPRS) #### Domain Amateur radio communications protocols, specifically the very popular packet radio protocol, APRS -- Automatic Packet Reporting System. #### Purpose * Demonstrate the power of functional data modeling in communications protocol applications in general, and APRS specifically * Provide a cross-platform, simple, easy to use application for sending and receiving APRS messages. * Provide an automated way to track race participants during amateur radio supported long-distance races (e.g, 24 Hours in the Old Pueblo, CV 50/50). * Provide a framework for developing custom APRS applications. * Expose more people to F# * Get more people involved in amateur radio #### The Highlights The code uses a lot of functional programming techniques and F# libraries to get things done, but here are the highlights: * [Designing with types](https://fsharpforfunandprofit.com/posts/designing-with-types-intro/) * Single case unions * Constrained strings * Making impossible states impossible * [Active patterns](https://fsharpforfunandprofit.com/posts/convenience-active-patterns/) for string parsing and validation * [Railway oriented programming](https://fsharpforfunandprofit.com/posts/recipe-part2/) * [Argu](http://fsprojects.github.io/Argu/) for command line parsing * [Expecto](https://github.com/haf/expecto) for testing * [Saturn](https://saturnframework.org/) for building the web app/web API/web service ## Amateur Radio and APRS ### What is amateur radio? What Wikipedia thinks it is: > Amateur radio, also known as ham radio, describes the use of radio frequency spectrum for purposes of non-commercial exchange of messages, wireless experimentation, self-training, private recreation, radiosport, contesting, and emergency communication What the [Radio Society of Great Britain]() thinks it is (great video introduction): > [![Packet Radio](https://img.youtube.com/vi/8x6x_6mDVlQ/0.jpg)](https://youtu.be/8x6x_6mDVlQ "Ham radio is awesome.") > _click the video to watch on You Tube_ ### What is APRS? What Wikipedia thinks it is: > *Automatic Packet Reporting System* is an amateur radio-based system for real time digital communications of information of immediate value in the local area. Data can include object Global Positioning System coordinates, weather station telemetry, text messages, announcements, queries, and other telemetry. This video is a nice demonstration of what you can do with APRS. The system demonstrated in the video is similar to my F# for APRS system design. > [![Packet Radio](https://img.youtube.com/vi/FJEVWMuz6Xg/0.jpg)](http://www.youtube.com/watch?v=FJEVWMuz6Xg "Kantronics Packet Radio Mail and BBS Operations") > _click the video to watch on You Tube_ ## System design FAPRS will act like a [APRS digipeater](http://www.nwclimate.org/aprs/digipeater/), that you can also command to transmit your own messages for certain purposes. ### Motivation, purpose, and use cases Hams often volunteer for middle-of-nowhere long distances races. You know the kind? Where 20 persons run 50 miles through the desert for fun? The participants need trail support and we Hams are best-equipped to run the communications system. We setup stations with mobile radios at known locations along the trail, make sure we can communicate with each other, and setup a process by which we share participant status and location, so we can keep track of them and request medical support if needed. It's great fun, and many of these events wouldn't happen if it weren't for us Hams. Most of the time we use voice to communicate. This works well enough, but I have worked enough of these races to know that it can be a challenge to keep track of all of the runners. The problem is this system requires a lot of coordination and sometimes we talk over each other or don't always hear the messages. The problems only increase as the number of participants increase. > It doesn't scale well. I wanted to develop a system using packet radio that could automatically send and receive participant status reports. ### Logical design #### Requirements * Store sent and received messages and re-send periodically * A management interface for entering participant status reports and general informational messages * Prioritize emergency messages and new reports * De-prioritize older messages and expire messages after a certain period of time * See a list of messages that were sent and received * Be accessible over WiFi so a keyboard and monitor are not required ### Physical design ![Imgur](https://i.imgur.com/9w47hfD.png) I designed this system with a few things in mind: * It will be used in remote locations * Low-cost * Highly mobile -- can carry all of the parts in a backpack easily * Compatible with common hand-held VHF radios like my Baofeng UV-82* * Low-power and possible to run off a portable solar cell The Raspberry Pi 3 will provide these services * WiFi hot-spot * DireWolf -- the Terminal Node Controller * Handles encoding and decoding packets and sending them out the audio port to the radio * FAPRS * Self-hosted web service providing * Management interface * Message scheduler and processor * CLI * Can be used to manually craft APRS messages for testing and troubleshooting * Database (`SQLite`) for storing sent and received messages * Wired to a VHF radio through the audio port * A USB sound card may be used * Need both an audio-in and audio-out (mic and speaker) so that messages can be both received and transmitted. ### My equipment * [RaspberryPi 3](https://www.raspberrypi.org/products/raspberry-pi-3-model-b/) * [Baofeng UV-82](https://baofengtech.com/uv82) * [BTech APRS K2 Cable (connect radio to RaspberryPi or laptop via the audio port)](https://baofengtech.com/aprs-k2-trrs-cable) * My laptop * My mobile phone (Android) ### DireWolf integration The DireWolf integration ended up being really simple because the developer provided a KISS utility that reads TNC2MON formatted messages from a file and converts them to KISS format to be transmitted via the TNC (in this case DireWolf is also running as a TNC). #### kissutil The DireWolf `kissutil` makes it easy to send and receive messages. The `kissutil` reads TNC2MON formated messages from files in a folder you specify and then sends them to the DireWolf TNC for sending out over the radio. The `kissutil` also writes received packets to a folder you specify. The Direwolf integration reads from, and writes to, these folders. The records are in the TNC2MON format, which I will talk about more later. ### Application architecture The overall pattern is following Onion Architecture/Clean Architecture. It consists of: * A core library where the data models live (`faprs.core`) * An infrastructure library where data operations and business logic happens (`faprs.infrastructure`) * A CLI for creating messages for the `kissutil` (`faprs.cli`) * The CLI uses [Argu](http://fsprojects.github.io/Argu/) * A web service that hosts a web interface for creating and displaying messages to send and receive through the `kissutil`(`faprs.service`) * The web service uses [Saturn](https://saturnframework.org/) * A SQL Lite database (database.sqlite) for storing sent and received messages. * Tests (`faprs.tests`) * Database migrations (`faprs.migrations`) ## APRS specification and implementation The APRS protocol was developed by [TAPR (Tucson Amateur Packet Radio)](https://www.tapr.org/aprs_information.html). There are 3 specification versions. * [APRS v1.0](http://www.aprs.org/doc/APRS101.PDF) * [APRS v1.1](http://www.aprs.org/aprs11.html) * [APRS v1.2](http://www.aprs.org/aprs12.html) ### TNC 2 Monitor format (TNC2MON) The `kissutil` accepts APRS packets in the TNC 2 Monitor format. This format is defined in the APRS version 1.0.1 specification under the section `Network Tunneling and Third-Party Digipeating`. FAPRS takes message details and outputs a TNC2MON formatted packet. It looks like this: > *SENDER*>*DESTINATION*,*PATH*:*MESSAGE* The packet consists of a source, a destination, a path, and a message. The message can be user-defined, but is most often a Position Report or a Weather Report. There are a number of Position Report formats as defined by the APRS spec. #### SENDER The `SENDER` is always the transmitting station's call sign. For example, my station's call sign is `KG7SIO`, because that was the call sign assigned to me by the FCC when I got my license. Since sender is a call sign, I created a CallSign type (in the `Common` namespace in `faprs.core`). I used a `single case union type` as described by Scott Wlashcin [here](https://fsharpforfunandprofit.com/posts/designing-with-types-single-case-dus/). ```fsharp //9 byte field type CallSign = private CallSign of string module CallSign = open System let create (s:string) = match (s.Trim()) with | c when not (String.IsNullOrEmpty(c)) && c.Length < 10 -> Some (CallSign c) | _ -> None // "Call Sign cannot be empty and must be 1 - 9 characters. See APRS 1.01." let value (CallSign s) = s.ToUpper() // MUST BE ALL CAPS ``` This makes it so that anywhere I need a CallSign I can be guaranteed to have a properly formated call sign. #### DESTINATION The `DESTINATION` can be the call sign of a particular station for which a message is intended, but `DESTINATION` is overloaded and can be used to pass on other encoded bits of information. One common usage is to identify the sending application and version number. You can see a list of TAPR approved to-calls [here](http://aprs.org/aprs11/tocalls.txt). By default, the `fapr.cli` will uses the DireWolf TOCALL, `APDW15`. Since destination is also a call sign, I can use the CallSign type. #### PATH The `PATH` is also known as the digipath, and specifies if and how an APRS package should be repeated (re-transmitted) when received by a digital repeater (digipeater). This is intended to avoid repeating packets redundantly, and reduce the amount of traffic on the APRS network. The digipeater will be configured to re-transmit according to the PATH depending on its location and general network conditions in order to help prevent network congestion. > PATH settings determine what kind and how many digipeaters will be used to deliver your packets to their destination. For example, `WIDE1-1`: > It requests that a "wide" digipeater (one with a wide coverage area, like on a mountaintop) repeat the packet, but only once; if a second "wide" digipeater should hear the rebroadcast packet, then the second digipeater wouldn't repeat it. The PATH part is best defined and explained [here](http://wa8lmf.net/DigiPaths/) and [here](https://ham.stackexchange.com/questions/6213/help-understanding-path-taken-by-aprs-packet?rq=1). ```fsharp type WIDEnN = | WIDE11 | WIDE21 | WIDE22 override this.ToString() = match this with | WIDE11 -> "WIDE1-1" | WIDE21 -> "WIDE2-1" | WIDE22 -> "WIDE2-2" static member fromString p = match p with | "WIDE1-1" -> WIDE11 | "WIDE2-1" -> WIDE21 | "WIDE2-2" -> WIDE22 | _ -> WIDE11 //Use this as the default //9 byte field //aka the UNPROTO path //http://wa8lmf.net/DigiPaths/index.htm#Recommended type Path = | WIDEnN of WIDEnN override this.ToString() = match this with | WIDEnN p -> match p with | WIDE11 -> WIDE11.ToString() | WIDE21 -> WIDE21.ToString() | WIDE22 -> WIDE22.ToString() ``` The PATH part used to have a number of types, but those were made obsolete and the recommend PATH is to only use one of the `WIDEnN` options. That is how I modeled it here, while also allowing for the possibility of supporting other PATH types. #### MESSAGE The `MESSAGE` is also known as the `payload`. This is the data you want to transmit `and the fun part`. The `MESSAGE` is also where it starts to get more complicated. As `APRS` started as a position reporting system, APRS specifies a number of standard message formats for identifying a station's position, but also provides for user-defined messages, weather reports, telemetry, and plain old messages (as if you were tweeting). FAPRS supports a number of message formats. I will cover some of them, here. ### APRS data formats All of the support message formats are defined by a union type. Each of the options has its own type. ```fsharp type Message = | Unformatted of UnformattedMessage | PositionReportWithoutTimeStamp of PositionReportWithoutTimeStamp | ParticipantStatusReport of Participant.ParticipantStatusReport | Unsupported of UnformattedMessage override this.ToString() = match this with | Unformatted m -> UnformattedMessage.value m | PositionReportWithoutTimeStamp r -> r.ToString() | ParticipantStatusReport r -> r.ToString() | Unsupported u -> UnformattedMessage.value u //This is where anything that cant be parsed will end up ``` #### Unformatted Message An unformatted message must start with `:`, and has a size constraint, but otherwise can contain anything. I used a single case union type for this one, too. ```fsharp type UnformattedMessage = private UnformattedMessage of string module UnformattedMessage = let create (m:string) = match (m.Trim()) with | m when m.Length <= 255 -> UnformattedMessage m //AX.25 field is 256 chars but the message has to accommodate the { for user defined messages | _ -> UnformattedMessage (m.Substring(0, 255)) let value (UnformattedMessage m) = sprintf ":%s" m ``` #### Lat/Long Position Report Format — without Timestamp This format is defined in `APRS 1.01` `6 TIME AND POSITION FORMATS` and `8 POSITION AND DF REPORT DATA FORMATS` It looks like this: ```text ! or | = | Latitude | / | Longitude | Symbol | Comment (max 43 chars) Bytes 1 8 1 9 1 0-43 ``` Example: ```text !4903.50N/07201.75W-Test ``` The latitude and longitude are expected to be in the APRS format defined in `6 TIME AND POSITION FORMATS`. I created a PositionReportWithoutTimeStamp record type that includes the fields of the position report. ```fsharp type PositionReportWithoutTimeStamp = { Position : Position Symbol : SymbolCode Comment : PositionReportComment } override this.ToString() = sprintf "=%s/%s%c%s" (FormattedLatitude.value this.Position.Latitude) (FormattedLongitude.value this.Position.Longitude) (this.Symbol.ToChar()) (PositionReportComment.value this.Comment) ``` `Position` is the latitude and longitude in APRS format. The `PositionReportWithoutTimeStamp` will also return the string representation of a position report. ##### Latitude and longitude Latitude and longitude take a decimal coordinate and convert it to the APRS format. Latitude is expressed as a fixed 8-character field, in degrees and decimal minutes (to two decimal places), followed by the letter N for north or S for south. Longitude is expressed as a fixed 9-character field, in degrees and decimal minutes (to two decimal places), followed by the letter E for east or W for west. I created two single case union types called `FormattedLatitude` and `FormattedLongitude` that do the conversion and create a formatted latitude or longitude. The hemisphere designation is further constrained by a type. ```fsharp type LatitudeHemisphere = | North | South member this.ToHemisphereChar() = match this with | North _ -> 'N' | South _ -> 'S' static member fromHemisphere h = match h with | 'N' -> Some LatitudeHemisphere.North | 'S' -> Some LatitudeHemisphere.South | _ -> None //"Latitude must be in northern (N) or southern (S) hemisphere." type FormattedLatitude = private FormattedLatitude of string module FormattedLatitude = let create (d:float) = let deg, min, sec = calcDegMinSec d FormattedLatitude (sprintf "%02i%02i.%02i%c" deg min sec (if d > 0.0 then (North.ToHemisphereChar()) else (South.ToHemisphereChar()))) let check (d:string) = FormattedLatitude d let value (FormattedLatitude d) = d ``` ##### Symbol APRS defines a list of symbols that can be rendered on a map and represent a station. There are a number of symbols defined, but FAPRS only supports some of them. ```fsharp //This is only a subset of the codes because I don't want to support all of them at this time type SymbolCode = | House | Bicycle | Balloon | Hospital | Jeep | Truck | Motorcycle | Jogger member this.ToChar() = match this with | House -> '-' | Bicycle -> 'b' | Balloon -> 'O' | Hospital -> 'h' | Jeep -> 'j' | Truck -> 'k' | Motorcycle -> '<' | Jogger -> '[' static member fromSymbol s = match s with | '-' -> Some House | 'b' -> Some Bicycle | 'O' -> Some Balloon | 'h' -> Some Hospital | 'j' -> Some Jeep | 'k' -> Some Truck | '<' -> Some Motorcycle | '[' -> Some Jogger | _ -> None ``` ##### Comment The `COMMENT` field has a size constraint, but otherwise can contain anything. I used a single case union type for this one, too. ```fsharp type PositionReportComment = private PositionReportComment of string module PositionReportComment = let create (s:string) = match (s.Trim()) with | s when s.Length < 44 -> Some (PositionReportComment s) | _ -> None let value (PositionReportComment c) = c //Was trimmed during create ``` #### Participant Status Report The `Participant Status Report` is a user-defined format that I created in order to facilitate tracking race participants. User-defined formating is defined in APRS 1.01 `18 USER DEFINED FORMATS`. The first 3 characters of a user-defined data format are the data identifiers. * { APRS Data Type Identifier. * U A one-character User ID. * X A one-character user-defined packet type. The APRS spec makes allowances for experimental user-defined formats. Experimental formats must start with `{{`. I chose `P` as the experimental user-defined data identifier. This makes the full identifier `{{P`. The APRS data needs to fit inside the [AX.25 information field](https://en.wikipedia.org/wiki/AX.25), which is defined as 1-256 bytes. Participant Status should include these parts: * User-defined data type * 253 chars max * Participant number (bib number) * Time last seen at comm station * Status (continued, injured, waiting for help, taking a break) ```text Participant Status Field TIMESTAMP PARTICIPANT-ID STATUS-1 STATUS-2 MESSAGE BYTES 8-fixed 5-fixed 1-fixed 1-fixed 0-238 ``` Example including the data identifier: ```text {{P100923450004211In good shape! ``` ```text IDENTIFIER TIMESTAMP PARTICIPANT-ID STATUS-1 STATUS-2 MESSAGE {{P 10092345 00042 1 1 In good shape! 2019-10-09 23:34 Continued Continued ``` The `TIMESTAMP` field is an APRS formatted timestamp. TIMESTAMP Month/Day/Hours/Minutes (MDHM) format is a fixed 8-character field, consisting of the month (01–12) and day-of-the-month (01–31), followed by the time in hours and minutes zulu. For example: 10092345 is 23 hours 45 minutes zulu on October 9th. I created a new record type called `ParticipantStatusReport` that defines 3 fields. ```fsharp type ParticipantStatusReport = { TimeStamp : RecordedOn ParticipantID : ParticipantID ParticipantStatus : ParticipantStatus } override this.ToString() = let (status1, status2, msg) = this.ParticipantStatus.ToStatusCombination() sprintf "{{P%s%s%i%i%s%s" (RecordedOn.value this.TimeStamp) (ParticipantID.value this.ParticipantID) status1 status2 msg ``` `RecordedOn` is a single case union type that converts a DateTime value to an APRS formatted timestamp. It creates a timestamp, and it will revert a timestamp back to a DateTime. ```fsharp type RecordedOn = private RecordedOn of string module RecordedOn = let revert (timestamp:string) = //TODO would this be better in an active pattern? let mm = (timestamp.Substring(0, 2)) let dd = (timestamp.Substring(2, 2)) let HH = (timestamp.Substring(4, 2)) let MM = (timestamp.Substring(6, 2)) let dt = sprintf "%i-%s-%sT%s:%s" DateTime.Today.Year mm dd HH MM DateTime.Parse(dt) let create (date:DateTime option) = match date with | Some d -> RecordedOn (sprintf "%02i%02i%02i%02i" d.Month d.Day d.Hour d.Minute) | None -> let utcNow = DateTime.Now RecordedOn (sprintf "%02i%02i%02i%02i" utcNow.Month utcNow.Day utcNow.Hour utcNow.Minute) let value (RecordedOn d) = d ``` `ParticipantID` is fixed-width, can be any characters, and is 0 padded to fill the empty space. ```fsharp type ParticipantID = private ParticipantID of string module ParticipantID = let create (nbr:string) = match nbr with | n when String.IsNullOrWhiteSpace(n) -> None | n when nbr.Length < 6 -> Some (ParticipantID (sprintf "%5s" n)) //Fixed width 5 chars | _ -> None let value (ParticipantID n) = n ``` `ParticipantStatus` is fixed-width and limited to a set of statuses in a combination of 1 or 2 status options, plus a free form message. I modeled this as a tuple of (status, status, message). Only `Injured` has a sub status combination. For example, an `Injured` participant could also be `Continued`, `Resting`, `NeedsEmergencySupport`, `DroppedOut`, `Unknown`. ```fsharp type ParticipantStatusMessage = private ParticipantStatusMessage of string module ParticipantStatusMessage = let create (s:string) = match (s.Trim()) with | s when s.Length <= 239 -> ParticipantStatusMessage s | _ -> ParticipantStatusMessage (s.Substring(0, 239)) let value (ParticipantStatusMessage s) = s type ParticipantStatus = | Continued of ParticipantStatusMessage | Injured of ParticipantStatus | Resting of ParticipantStatusMessage | NeedsEmergencySupport of ParticipantStatusMessage | Completed of ParticipantStatusMessage | DroppedOut of ParticipantStatusMessage | Unknown of ParticipantStatusMessage member this.ToStatusCombination() = match this with | Continued m -> (1, 1, ParticipantStatusMessage.value m) | Injured s -> match s with | Continued m -> (2, 1, ParticipantStatusMessage.value m) | Resting m -> (2, 3, ParticipantStatusMessage.value m) | NeedsEmergencySupport m -> (2, 4, ParticipantStatusMessage.value m) | DroppedOut m -> (2, 6, ParticipantStatusMessage.value m) | Unknown m -> (2, 0, ParticipantStatusMessage.value m) | _ -> (2, 0, String.Empty) | Resting m -> (3, 3, ParticipantStatusMessage.value m) | NeedsEmergencySupport m -> (4, 4, ParticipantStatusMessage.value m) | Completed m -> (5, 5, ParticipantStatusMessage.value m) | DroppedOut m | Unknown m -> (0, 0, ParticipantStatusMessage.value m) member this.ToOptionName () = match this with | Continued s -> "Continued" | Injured s -> "Injured" | Resting s -> "Resting" | NeedsEmergencySupport s -> "Needs Emergency Support" | Completed s -> "Completed" | DroppedOut s -> "Dropped Out" | Unknown s -> "Unknown" static member fromStatusCombo s = match s with | (1, 1, m) -> (Continued (ParticipantStatusMessage.create m)) | (2, 1, m) -> (Injured (Continued (ParticipantStatusMessage.create m))) | (2, 3, m) -> (Injured (Resting (ParticipantStatusMessage.create m))) | (2, 4, m) -> (Injured (NeedsEmergencySupport (ParticipantStatusMessage.create m))) | (2, 0, m) -> (Injured (Unknown (ParticipantStatusMessage.create m))) | (3, 3, m) -> (Resting (ParticipantStatusMessage.create m)) | (4, 4, m) -> (NeedsEmergencySupport (ParticipantStatusMessage.create m)) | (5, 5, m) -> (Completed (ParticipantStatusMessage.create m)) | (6, 6, m) -> (DroppedOut (ParticipantStatusMessage.create m)) | (0, 0, m) -> (Unknown (ParticipantStatusMessage.create m)) | (_, _, m) -> (Unknown (ParticipantStatusMessage.create m)) ``` #### The final participant status report data frame The final output for the `kissutil` will look like this: > KG7SIO>APDW15,WIDE1-1:{{P100923450004211In good shape! ## Parsing received messages FAPRS can produce an APRS message, and it can parse a TNC2MON formatted frame with a number of APRS data formats. To do this I used [Active Patterns](https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/active-patterns). The parser assumes the frames were produced by the DireWolf `kissutil`, which are in this format: > [0] K1NRO-1>APDW15,WIDE2-2:!4238.80NS07105.63W#PHG5630 This is the same as what FAPRS produces for the `kissutil`, but includes the channel on which the message was received -- `[0]`. ### Start by getting the frame without the channel ```fsharp //Remove the channel from the frame let (|Frame|_|) (record:string) = match record with | r when String.IsNullOrWhiteSpace(r) -> None | r when r.IndexOf(" ") < 1 -> None //maybe return r because there was no channel and that's ok? | r when (r.Substring(r.IndexOf(" "))).Trim().Length > 0 -> Some ((r.Substring(r.IndexOf(" "))).Trim()) | _ -> None ``` ### Get the address field -- the sender and destination ```fsharp let (|Address|_|) (frame:string) = if frame.IndexOf(":") < 1 then None else Some (frame.Substring(0, frame.IndexOf(":"))) ``` ### Get the sender and destination out of the Address ```fsharp let (|Sender|_|) (address:string) = if address.IndexOf(">") < 1 then None else Some (address.Substring(0, address.IndexOf(">"))) let (|Destination|_|) (address:string) = if address.IndexOf(">") < 1 || address.IndexOf(",") < 1 then None else Some (address.Substring(address.IndexOf(">") + 1, address.IndexOf(",") - address.IndexOf(">") - 1)) ``` ### Get the Path out of the Frame ```fsharp let (|Path|_|) (address:string) = if not (address.IndexOf(">") = -1) && address.IndexOf(",") > address.IndexOf(">") then Some (address.Substring(address.IndexOf(",") + 1).Split(',')) else None ``` ### Get the Message out of the Frame ```fsharp let (|Message|_|) (frame:string) = if frame.IndexOf(":") < 1 then None else Some (frame.Substring(frame.IndexOf(":") + 1)) ``` ### Parse a position report #### Get the Latitude out of the Message ```fsharp let (|Latitude|_|) (msg:string) = let parseLatitude (posRpt:string) = let lat = posRpt.Substring(1, 8) match lat.EndsWith("N"), lat.EndsWith("S") with | true, false -> Some lat | false, true -> Some lat | _ -> None match getAPRSDataTypeIdentifier (msg.Substring(0,1)) with | Some id -> match id with | PositionReportWithoutTimeStampWithMessaging -> (parseLatitude msg) | PositionReportWithoutTimeStampNoMessaging -> (parseLatitude msg) | _ -> None | None -> None //We do not have a position report and therefore no latitude ``` #### Get the Longitude out of the Message ```fsharp let (|Longitude|_|) (msg:string) = let parseLongitude (posRpt:string) = let lon = posRpt.Substring(10, 9) match lon.EndsWith("W"), lon.EndsWith("E") with | true, false -> Some lon | false, true -> Some lon | _ -> None match msg.Substring(9,1) with | "/" -> parseLongitude msg | _ -> None ``` #### Get the Symbol out of the Message ```fsharp let (|Symbol|_|) (msg:string) = //TODO check that the previous char was a W or E meaning that it was probably and APRS lat/lon match msg.Substring(18,1) with | "W" -> SymbolCode.fromSymbol (msg.Substring(19,1).ToCharArray().[0]) // getSymbolCode (msg.Substring(19,1).ToCharArray().[0]) | "E" -> SymbolCode.fromSymbol (msg.Substring(19,1).ToCharArray().[0]) | _ -> None ``` #### Get the Comment out of the Message ```fsharp let (|Comment|_|) (symbol:char) (msg:string) = let comment = msg.Substring(msg.IndexOf(symbol) + 1).Trim() if comment = String.Empty then None else Some comment ``` #### The Tests To see the active patterns in action, check out `faprs.tests` `TNC2MONActivePatternsTests`. For example ```fsharp testCase "Can get message part of well formed frame with message" <| fun _ -> let result = match "[0] KG7SIO-7>APRD15,WIDE1-1,TCPXX*,qAX,CWOP-2:=03216.4N/011057.3Wb,b>,lah:blah /fishcakes" with | TNC2MonActivePatterns.Message m -> m | _ -> String.Empty Expect.equal result "=03216.4N/011057.3Wb,b>,lah:blah /fishcakes" "Message does not match" ``` ## Sending messages and a demo (proof of concept) To send messages we can use faprs.cli, but it only supports the `unformatted` and `position report without timestamp` APRS data formats at the moment. For the demo I am sending a position report. > See [the project's README](https://github.com/MarneeDear/FAPRS/blob/master/README.md) for more on how to run and use FAPRS I will use `dotnet run` to run the CLI command, like this: ```bash dotnet run --project src/faprs.cli/ -- --save-to XMIT --sender KG7SIO-7 --destination KG7SIL --path WIDE1-1 --rpt latitude 32.2217 longitude -110.9265 symbol b comment "My submission for the applied F# challenge." ``` The CLI takes latitude and longitude in decimal degrees and converts it to APRS format. In this demo I am expecting an iGate and digipeater in my area (`N7HND`) to receive my signal and forward it to [aprs.fi](https://aprs.fi), where I will be able to see my station on a map, and see the packets that were received. ### Steps: 1. Tune my radio to the [recommended APRS frequency](http://www.aprs.org/) `144.390` so other stations near me can pick up my signal. 2. Attach the radio to the computer audio. I can select the "headset" option which lets me transmit and receive 3. Start `DireWolf` 4. Start the `kissutil` 5. Enter the CLI command 6. Check that `DireWolf` and the `kissutil` read the file that was generated and sent the message 7. Check with aprs.fi to see if the message made it through the local digipeater ![Imgur](https://i.imgur.com/QoR9wYK.gif) ![Imgur](https://i.imgur.com/ksav8IY.png) ## Other Resources > [![History of APRS](https://img.youtube.com/vi/OgFBXfwmKYc/0.jpg)](https://www.youtube.com/watch?v=OgFBXfwmKYc "Everything Ham Radio Podcast") > _click the video to watch on You Tube_ ### Prior Art [Python APRS Module](https://github.com/ampledata/aprs) [APRSdroid](https://aprsdroid.org/)
json metadata{"tags":["fsharp","radio","aprs"],"image":["https://img.youtube.com/vi/8x6x_6mDVlQ/0.jpg","https://img.youtube.com/vi/FJEVWMuz6Xg/0.jpg","https://i.imgur.com/9w47hfD.png","https://i.imgur.com/QoR9wYK.gif","https://i.imgur.com/ksav8IY.png","https://img.youtube.com/vi/OgFBXfwmKYc/0.jpg"],"links":["http://foundation.fsharp.org/applied_fsharp_challenge","https://github.com/MarneeDear/FAPRS","https://fsharpforfunandprofit.com/posts/designing-with-types-intro/","https://fsharpforfunandprofit.com/posts/convenience-active-patterns/","https://fsharpforfunandprofit.com/posts/recipe-part2/","http://fsprojects.github.io/Argu/","https://github.com/haf/expecto","https://saturnframework.org/","https://youtu.be/8x6x_6mDVlQ","http://www.youtube.com/watch?v=FJEVWMuz6Xg","http://www.nwclimate.org/aprs/digipeater/","https://www.raspberrypi.org/products/raspberry-pi-3-model-b/","https://baofengtech.com/uv82","https://baofengtech.com/aprs-k2-trrs-cable","https://www.tapr.org/aprs_information.html","http://www.aprs.org/doc/APRS101.PDF","http://www.aprs.org/aprs11.html","http://www.aprs.org/aprs12.html","https://fsharpforfunandprofit.com/posts/designing-with-types-single-case-dus/","http://aprs.org/aprs11/tocalls.txt","http://wa8lmf.net/DigiPaths/","https://ham.stackexchange.com/questions/6213/help-understanding-path-taken-by-aprs-packet?rq=1","https://en.wikipedia.org/wiki/AX.25","https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/active-patterns","https://github.com/MarneeDear/FAPRS/blob/master/README.md","https://aprs.fi","http://www.aprs.org/","https://www.youtube.com/watch?v=OgFBXfwmKYc","https://github.com/ampledata/aprs","https://aprsdroid.org/"],"app":"steemit/0.1","format":"markdown"}
Transaction InfoBlock #33052502/Trx b85eb3f8df4c8e7fdaadd421a319bb273794cbda
View Raw JSON Data
{
  "trx_id": "b85eb3f8df4c8e7fdaadd421a319bb273794cbda",
  "block": 33052502,
  "trx_in_block": 1,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2019-05-19T19:31:36",
  "op": [
    "comment",
    {
      "parent_author": "",
      "parent_permlink": "fsharp",
      "author": "marnee",
      "permlink": "f-for-aprs",
      "title": "F# for APRS",
      "body": "A system for sending and receiving APRS messages integrated with DireWolf, and built on .NET Core in F#.\n\n_This is also my submission for the [Applied F# Challenge](http://foundation.fsharp.org/applied_fsharp_challenge) - F# in your organization or domain category._\n\n## Applied F# Challenge\n\n#### Author\n\nMarnee Dearman (KG7SIO)\n\n#### Github Repository\n\n[MarneeDear/FAPRS](https://github.com/MarneeDear/FAPRS)\n\n#### Domain\n\nAmateur radio communications protocols, specifically the very popular packet radio protocol, APRS -- Automatic Packet Reporting System. \n\n#### Purpose\n\n* Demonstrate the power of functional data modeling in communications protocol applications in general, and APRS specifically\n* Provide a cross-platform, simple, easy to use application for sending and receiving APRS messages.\n* Provide an automated way to track race participants during amateur radio supported long-distance races (e.g, 24 Hours in the Old Pueblo, CV 50/50). \n* Provide a framework for developing custom APRS applications.\n* Expose more people to F#\n* Get more people involved in amateur radio\n\n#### The Highlights\n\nThe code uses a lot of functional programming techniques and F# libraries to get things done, but here are the highlights:\n\n* [Designing with types](https://fsharpforfunandprofit.com/posts/designing-with-types-intro/)\n  * Single case unions\n  * Constrained strings\n  * Making impossible states impossible\n* [Active patterns](https://fsharpforfunandprofit.com/posts/convenience-active-patterns/) for string parsing and validation\n* [Railway oriented programming](https://fsharpforfunandprofit.com/posts/recipe-part2/)\n* [Argu](http://fsprojects.github.io/Argu/) for command line parsing\n* [Expecto](https://github.com/haf/expecto) for testing\n* [Saturn](https://saturnframework.org/) for building the web app/web API/web service\n\n\n## Amateur Radio and APRS\n\n### What is amateur radio?\n\nWhat Wikipedia thinks it is:\n\n> Amateur radio, also known as ham radio, describes the use of radio frequency spectrum for purposes of non-commercial exchange of messages, wireless experimentation, self-training, private recreation, radiosport, contesting, and emergency communication\n\nWhat the [Radio Society of Great Britain]() thinks it is (great video introduction):\n\n> [![Packet Radio](https://img.youtube.com/vi/8x6x_6mDVlQ/0.jpg)](https://youtu.be/8x6x_6mDVlQ \"Ham radio is awesome.\")\n\n> _click the video to watch on You Tube_\n\n### What is APRS?\n\nWhat Wikipedia thinks it is:\n\n> *Automatic Packet Reporting System* is an amateur radio-based system for real time digital communications of information of immediate value in the local area. Data can include object Global Positioning System coordinates, weather station telemetry, text messages, announcements, queries, and other telemetry.\n\nThis video is a nice demonstration of what you can do with APRS. The system demonstrated in the video is similar to my F# for APRS system design.\n\n> [![Packet Radio](https://img.youtube.com/vi/FJEVWMuz6Xg/0.jpg)](http://www.youtube.com/watch?v=FJEVWMuz6Xg \"Kantronics Packet Radio Mail and BBS Operations\")\n\n> _click the video to watch on You Tube_\n\n## System design\n\nFAPRS will act like a [APRS digipeater](http://www.nwclimate.org/aprs/digipeater/), that you can also command to transmit your own messages for certain purposes.\n\n### Motivation, purpose, and use cases\n\nHams often volunteer for middle-of-nowhere long distances races. You know the kind? Where 20 persons run 50 miles through the desert for fun? The participants need trail support and we Hams are best-equipped to run the communications system. We setup stations with mobile radios at known locations along the trail, make sure we can communicate with each other, and setup a process by which we share participant status and location, so we can keep track of them and request medical support if needed. It's great fun, and many of these events wouldn't happen if it weren't for us Hams.\n\nMost of the time we use voice to communicate. This works well enough, but I have worked enough of these races to know that it can be a challenge to keep track of all of the runners. The problem is this system requires a lot of coordination and sometimes we talk over each other or don't always hear the messages. The problems only increase as the number of participants increase. \n\n> It doesn't scale well.\n\nI wanted to develop a system using packet radio that could automatically send and receive participant status reports.\n\n### Logical design\n\n#### Requirements \n\n* Store sent and received messages and re-send periodically\n* A management interface for entering participant status reports and general informational messages\n* Prioritize emergency messages and new reports\n* De-prioritize older messages and expire messages after a certain period of time\n* See a list of messages that were sent and received\n* Be accessible over WiFi so a keyboard and monitor are not required\n\n### Physical design\n\n![Imgur](https://i.imgur.com/9w47hfD.png)\n\nI designed this system with a few things in mind:\n\n* It will be used in remote locations\n* Low-cost \n* Highly mobile -- can carry all of the parts in a backpack easily\n* Compatible with common hand-held VHF radios like my Baofeng UV-82* \n* Low-power and possible to run off a portable solar cell\n\nThe Raspberry Pi 3 will provide these services\n\n* WiFi hot-spot\n* DireWolf -- the Terminal Node Controller \n    * Handles encoding and decoding packets and sending them out the audio port to the radio\n* FAPRS\n    * Self-hosted web service providing\n        * Management interface\n        * Message scheduler and processor\n    * CLI\n        * Can be used to manually craft APRS messages for testing and troubleshooting\n    * Database (`SQLite`) for storing sent and received messages\n* Wired to a VHF radio through the audio port\n    * A USB sound card may be used\n    * Need both an audio-in and audio-out (mic and speaker) so that messages can be both received and transmitted.\n\n### My equipment\n\n * [RaspberryPi 3](https://www.raspberrypi.org/products/raspberry-pi-3-model-b/)\n * [Baofeng UV-82](https://baofengtech.com/uv82)\n * [BTech APRS K2 Cable (connect radio to RaspberryPi or laptop via the audio port)](https://baofengtech.com/aprs-k2-trrs-cable)\n * My laptop\n * My mobile phone (Android)\n\n### DireWolf integration\n\nThe DireWolf integration ended up being really simple because the developer provided a KISS utility that reads TNC2MON formatted messages from a file and converts them to KISS format to be transmitted via the TNC (in this case DireWolf is also running as a TNC).\n\n#### kissutil\n\nThe DireWolf `kissutil` makes it easy to send and receive messages. The `kissutil` reads TNC2MON formated messages from files in a folder you specify and then sends them to the DireWolf TNC for sending out over the radio. The `kissutil` also writes received packets to a folder you specify. The Direwolf integration reads from, and writes to, these folders.\n\nThe records are in the TNC2MON format, which I will talk about more later.\n\n### Application architecture\n\nThe overall pattern is following Onion Architecture/Clean Architecture. It consists of:\n\n* A core library where the data models live (`faprs.core`)\n* An infrastructure library where data operations and business logic happens (`faprs.infrastructure`)\n* A CLI for creating messages for the `kissutil` (`faprs.cli`)\n    * The CLI uses [Argu](http://fsprojects.github.io/Argu/)\n* A web service that hosts a web interface for creating and displaying messages to send and receive through the `kissutil`(`faprs.service`)\n    * The web service uses [Saturn](https://saturnframework.org/)\n* A SQL Lite database (database.sqlite) for storing sent and received messages.\n* Tests (`faprs.tests`)\n* Database migrations (`faprs.migrations`)\n\n## APRS specification and implementation\n\nThe APRS protocol was developed by [TAPR (Tucson Amateur Packet Radio)](https://www.tapr.org/aprs_information.html). \n\nThere are 3 specification versions.\n\n* [APRS v1.0](http://www.aprs.org/doc/APRS101.PDF)\n* [APRS v1.1](http://www.aprs.org/aprs11.html)\n* [APRS v1.2](http://www.aprs.org/aprs12.html)\n\n### TNC 2 Monitor format (TNC2MON)\n\nThe `kissutil` accepts APRS packets in the TNC 2 Monitor format. This format is defined in the\nAPRS version 1.0.1 specification under the section `Network Tunneling and Third-Party Digipeating`. \n\nFAPRS takes message details and outputs a TNC2MON formatted packet.\n\nIt looks like this:\n\n> *SENDER*>*DESTINATION*,*PATH*:*MESSAGE*\n\nThe packet consists of a source, a destination, a path, and a message. The message can be user-defined, but is most often a Position Report or a Weather Report. There are a number of Position Report formats as defined by the APRS spec. \n\n#### SENDER\n\nThe `SENDER` is always the transmitting station's call sign. \n\nFor example, my station's call sign is `KG7SIO`, because that was the call sign assigned to me by the FCC when I got my license.\n\nSince sender is a call sign, I created a CallSign type (in the `Common` namespace in `faprs.core`). I used a `single case union type` as described by Scott Wlashcin [here](https://fsharpforfunandprofit.com/posts/designing-with-types-single-case-dus/).\n\n```fsharp\n//9 byte field\ntype CallSign = private CallSign of string          \nmodule CallSign =\n    open System\n\n    let create (s:string) = \n        match (s.Trim()) with\n        | c when not (String.IsNullOrEmpty(c)) && c.Length < 10     -> Some (CallSign c)\n        | _                                                         -> None // \"Call Sign cannot be empty and must be 1 - 9 characters. See APRS 1.01.\"\n    let value (CallSign s) = s.ToUpper() // MUST BE ALL CAPS        \n```\n\nThis makes it so that anywhere I need a CallSign I can be guaranteed to have a properly formated call sign.\n\n#### DESTINATION\n\nThe `DESTINATION` can be the call sign of a particular station for which a message is intended, but `DESTINATION` is overloaded and can be used to pass on other encoded bits of information. One common usage is to identify the sending application and version number. You can see a list of TAPR approved to-calls [here](http://aprs.org/aprs11/tocalls.txt).\n\nBy default, the `fapr.cli` will uses the DireWolf TOCALL, `APDW15`.\n\nSince destination is also a call sign, I can use the CallSign type.\n\n#### PATH\n\nThe `PATH` is also known as the digipath, and specifies if and how an APRS package should be repeated (re-transmitted) when received by a digital repeater (digipeater). This is intended to avoid repeating packets redundantly, and reduce the amount of traffic on the APRS network. The digipeater will be configured to re-transmit according to the PATH depending on its location and general network conditions in order to help prevent network congestion. \n\n> PATH settings determine what kind and how many digipeaters will be used to deliver your packets to their destination.\n\nFor example, `WIDE1-1`:\n\n> It requests that a \"wide\" digipeater (one with a wide coverage area, like on a mountaintop) repeat the packet, but only once; if a second \"wide\" digipeater should hear the rebroadcast packet, then the second digipeater wouldn't repeat it.\n\nThe PATH part is best defined and explained [here](http://wa8lmf.net/DigiPaths/) and [here](https://ham.stackexchange.com/questions/6213/help-understanding-path-taken-by-aprs-packet?rq=1).\n\n```fsharp\ntype WIDEnN =\n    | WIDE11\n    | WIDE21\n    | WIDE22\n    override this.ToString() =\n        match this with \n        | WIDE11    -> \"WIDE1-1\"\n        | WIDE21    -> \"WIDE2-1\"\n        | WIDE22    -> \"WIDE2-2\"\n    static member fromString p =\n        match p with\n        | \"WIDE1-1\" -> WIDE11\n        | \"WIDE2-1\" -> WIDE21\n        | \"WIDE2-2\" -> WIDE22\n        | _         -> WIDE11 //Use this as the default\n\n//9 byte field\n//aka the UNPROTO path\n//http://wa8lmf.net/DigiPaths/index.htm#Recommended\ntype Path =\n    | WIDEnN of WIDEnN\n    override this.ToString() =\n        match this with\n        | WIDEnN p ->   match p with\n                        | WIDE11    -> WIDE11.ToString()\n                        | WIDE21    -> WIDE21.ToString()\n                        | WIDE22    -> WIDE22.ToString()\n```\n\nThe PATH part used to have a number of types, but those were made obsolete and the recommend PATH is to only use one of the `WIDEnN` options. That is how I modeled it here, while also allowing for the possibility of supporting other PATH types.\n\n#### MESSAGE\n\nThe `MESSAGE` is also known as the `payload`. This is the data you want to transmit `and the fun part`.\n\nThe `MESSAGE` is also where it starts to get more complicated. As `APRS` started as a position reporting system, APRS specifies a number of standard message formats for identifying a station's position, but also provides for user-defined messages, weather reports, telemetry, and plain old messages (as if you were tweeting). \n\nFAPRS supports a number of message formats. I will cover some of them, here.\n\n### APRS data formats\n\nAll of the support message formats are defined by a union type. Each of the options has its own type.\n\n```fsharp\ntype Message =\n    | Unformatted                       of UnformattedMessage\n    | PositionReportWithoutTimeStamp    of PositionReportWithoutTimeStamp\n    | ParticipantStatusReport           of Participant.ParticipantStatusReport\n    | Unsupported                       of UnformattedMessage\n    override this.ToString() =\n        match this with \n        | Unformatted m                     -> UnformattedMessage.value m\n        | PositionReportWithoutTimeStamp r  -> r.ToString()\n        | ParticipantStatusReport r         -> r.ToString()\n        | Unsupported u                     -> UnformattedMessage.value u //This is where anything that cant be parsed will end up\n```\n\n#### Unformatted Message\n\nAn unformatted message must start with `:`, and has a size constraint, but otherwise can contain anything. I used a single case union type for this one, too.\n\n```fsharp\ntype UnformattedMessage = private UnformattedMessage of string\nmodule UnformattedMessage =\n    let create (m:string) =\n        match (m.Trim()) with\n        | m when m.Length <= 255    -> UnformattedMessage m //AX.25 field is 256 chars but the message has to accommodate the { for user defined messages\n        | _                         -> UnformattedMessage (m.Substring(0, 255)) \n    let value (UnformattedMessage m) = sprintf \":%s\" m\n```\n\n#### Lat/Long Position Report Format — without Timestamp\n\nThis format is defined in `APRS 1.01` `6 TIME AND POSITION FORMATS` and `8 POSITION AND DF REPORT DATA FORMATS`\n\nIt looks like this:\n\n```text\n        ! or |\n        =    | Latitude |  /  | Longitude | Symbol | Comment (max 43 chars)\nBytes    1         8       1        9          1           0-43\n```\n\nExample:\n\n```text\n!4903.50N/07201.75W-Test\n```\n\nThe latitude and longitude are expected to be in the APRS format defined in `6 TIME AND POSITION FORMATS`.\n\nI created a PositionReportWithoutTimeStamp record type that includes the fields of the position report.\n\n```fsharp\ntype PositionReportWithoutTimeStamp =\n    {\n        Position    : Position\n        Symbol      : SymbolCode\n        Comment     : PositionReportComment\n    }\n    override this.ToString() =\n        sprintf \"=%s/%s%c%s\" (FormattedLatitude.value this.Position.Latitude) (FormattedLongitude.value this.Position.Longitude) (this.Symbol.ToChar()) (PositionReportComment.value this.Comment)  \n```\n\n`Position` is the latitude and longitude in APRS format.\n\nThe `PositionReportWithoutTimeStamp` will also return the string representation of a position report.\n\n##### Latitude and longitude \n\nLatitude and longitude take a decimal coordinate and convert it to the APRS format.\n\n    Latitude is expressed as a fixed 8-character field, in degrees and decimal\n    minutes (to two decimal places), followed by the letter N for north or S for\n    south.\n\n    Longitude is expressed as a fixed 9-character field, in degrees and decimal\n    minutes (to two decimal places), followed by the letter E for east or W for\n    west.\n\nI created two single case union types called `FormattedLatitude` and `FormattedLongitude` that do the conversion and create a formatted latitude or longitude. The hemisphere designation is further constrained by a type.\n\n```fsharp\ntype LatitudeHemisphere =\n    | North     \n    | South     \n    member this.ToHemisphereChar() =\n        match this with\n        | North _   -> 'N'\n        | South _   -> 'S'\n    static member fromHemisphere h =\n        match h with\n        | 'N'   -> Some LatitudeHemisphere.North\n        | 'S'   -> Some LatitudeHemisphere.South\n        | _     -> None //\"Latitude must be in northern (N) or southern (S) hemisphere.\"\n\ntype FormattedLatitude = private FormattedLatitude of string\nmodule FormattedLatitude =\n    let create (d:float) =\n        let deg, min, sec = calcDegMinSec d\n        FormattedLatitude (sprintf \"%02i%02i.%02i%c\" deg min sec (if d > 0.0 then (North.ToHemisphereChar()) else (South.ToHemisphereChar())))\n    let check (d:string) =\n        FormattedLatitude d \n    let value (FormattedLatitude d) = d\n```\n\n##### Symbol\n\nAPRS defines a list of symbols that can be rendered on a map and represent a station. There are a number of symbols defined, but FAPRS only supports some of them.\n\n```fsharp\n    //This is only a subset of the codes because I don't want to support all of them at this time\ntype SymbolCode =\n    | House \n    | Bicycle \n    | Balloon \n    | Hospital\n    | Jeep \n    | Truck\n    | Motorcycle\n    | Jogger\n    member this.ToChar() =\n        match this with\n        | House         -> '-'\n        | Bicycle       -> 'b'\n        | Balloon       -> 'O'\n        | Hospital      -> 'h'\n        | Jeep          -> 'j'\n        | Truck         -> 'k'\n        | Motorcycle    -> '<'\n        | Jogger        -> '['\n    static member fromSymbol s =\n        match s with\n        | '-' -> Some House\n        | 'b' -> Some Bicycle\n        | 'O' -> Some Balloon\n        | 'h' -> Some Hospital\n        | 'j' -> Some Jeep\n        | 'k' -> Some Truck\n        | '<' -> Some Motorcycle\n        | '[' -> Some Jogger\n        | _   -> None\n```\n\n##### Comment\n\nThe `COMMENT` field has a size constraint, but otherwise can contain anything. I used a single case union type for this one, too.\n\n```fsharp\ntype PositionReportComment = private PositionReportComment of string\nmodule PositionReportComment =\n    let create (s:string) =\n        match (s.Trim()) with\n        | s when s.Length < 44  -> Some (PositionReportComment s)\n        | _                     -> None \n\n    let value (PositionReportComment c) = c //Was trimmed during create\n```\n\n#### Participant Status Report\n\nThe `Participant Status Report` is a user-defined format that I created in order to facilitate tracking race participants. \n\nUser-defined formating is defined in APRS 1.01 `18 USER DEFINED FORMATS`. The first 3 characters of a user-defined data format are the data identifiers. \n\n* { APRS Data Type Identifier.\n* U A one-character User ID.\n* X A one-character user-defined packet type.\n\nThe APRS spec makes allowances for experimental user-defined formats. Experimental formats must start with `{{`. I chose `P` as the experimental user-defined data identifier. This makes the full identifier `{{P`.\n\nThe APRS data needs to fit inside the [AX.25 information field](https://en.wikipedia.org/wiki/AX.25), which is defined as 1-256 bytes. \n\nParticipant Status should include these parts:\n* User-defined data type\n* 253 chars max\n* Participant number (bib number)\n* Time last seen at comm station\n* Status (continued, injured, waiting for help, taking a break) \n\n```text\nParticipant Status Field\n        TIMESTAMP   PARTICIPANT-ID      STATUS-1    STATUS-2    MESSAGE\nBYTES   8-fixed     5-fixed             1-fixed     1-fixed     0-238 \n```\n\nExample including the data identifier:\n\n```text\n{{P100923450004211In good shape!\n```\n\n```text\nIDENTIFIER  TIMESTAMP   PARTICIPANT-ID  STATUS-1    STATUS-2    MESSAGE\n{{P         10092345    00042           1           1           In good shape!\n            2019-10-09 23:34            Continued   Continued\n```\n\nThe `TIMESTAMP` field is an APRS formatted timestamp.\n\n    TIMESTAMP\n    Month/Day/Hours/Minutes (MDHM) format is a fixed 8-character field,\n    consisting of the month (01–12) and day-of-the-month (01–31), followed by\n    the time in hours and minutes zulu. For example: 10092345 is 23 hours 45 minutes zulu on October 9th.\n\nI created a new record type called `ParticipantStatusReport` that defines 3 fields.\n\n```fsharp\ntype ParticipantStatusReport =\n    {\n        TimeStamp           : RecordedOn\n        ParticipantID       : ParticipantID\n        ParticipantStatus   : ParticipantStatus\n    }\n    override this.ToString() =\n        let (status1, status2, msg) = this.ParticipantStatus.ToStatusCombination()\n        sprintf \"{{P%s%s%i%i%s%s\" (RecordedOn.value this.TimeStamp) (ParticipantID.value this.ParticipantID) status1 status2 msg\n```\n\n`RecordedOn` is a single case union type that converts a DateTime value to an APRS formatted timestamp. It creates a timestamp, and it will revert a timestamp back to a DateTime.\n\n```fsharp\ntype RecordedOn = private RecordedOn of string\nmodule RecordedOn =\n    let revert (timestamp:string) = //TODO would this be better in an active pattern?\n        let mm = (timestamp.Substring(0, 2))\n        let dd = (timestamp.Substring(2, 2))\n        let HH = (timestamp.Substring(4, 2))\n        let MM = (timestamp.Substring(6, 2))\n        let dt = sprintf \"%i-%s-%sT%s:%s\" DateTime.Today.Year mm dd HH MM\n        DateTime.Parse(dt)\n    let create (date:DateTime option) =\n        match date with\n        | Some d    -> RecordedOn (sprintf \"%02i%02i%02i%02i\" d.Month d.Day d.Hour d.Minute)\n        | None      -> let utcNow = DateTime.Now\n                        RecordedOn (sprintf \"%02i%02i%02i%02i\" utcNow.Month utcNow.Day utcNow.Hour utcNow.Minute)\n    let value (RecordedOn d) = d\n```\n\n`ParticipantID` is fixed-width, can be any characters, and is 0 padded to fill the empty space.\n\n```fsharp\ntype ParticipantID = private ParticipantID of string\nmodule ParticipantID =\n    let create (nbr:string) =\n        match nbr with \n        | n when String.IsNullOrWhiteSpace(n)   -> None\n        | n when nbr.Length < 6                 -> Some (ParticipantID (sprintf \"%5s\" n)) //Fixed width 5 chars\n        | _                                     -> None \n    let value (ParticipantID n) = n\n```\n\n`ParticipantStatus` is fixed-width and limited to a set of statuses in a combination of 1 or 2 status options, plus a free form message. I modeled this as a tuple of (status, status, message).\n\nOnly `Injured` has a sub status combination. For example, an `Injured` participant could also be `Continued`, `Resting`, `NeedsEmergencySupport`, `DroppedOut`, `Unknown`.\n\n```fsharp\ntype ParticipantStatusMessage = private ParticipantStatusMessage of string\nmodule ParticipantStatusMessage =\n    let create (s:string) =\n        match (s.Trim()) with\n        | s when s.Length <= 239 -> ParticipantStatusMessage s\n        | _ -> ParticipantStatusMessage (s.Substring(0, 239))\n    let value (ParticipantStatusMessage s) = s\n\ntype ParticipantStatus =\n    | Continued                 of ParticipantStatusMessage\n    | Injured                   of ParticipantStatus\n    | Resting                   of ParticipantStatusMessage\n    | NeedsEmergencySupport     of ParticipantStatusMessage\n    | Completed                 of ParticipantStatusMessage\n    | DroppedOut                of ParticipantStatusMessage\n    | Unknown                   of ParticipantStatusMessage\n    member this.ToStatusCombination() =\n        match this with\n        | Continued m       -> (1, 1, ParticipantStatusMessage.value m)\n        | Injured s         ->  match s with\n                                | Continued m               -> (2, 1, ParticipantStatusMessage.value m)\n                                | Resting m                 -> (2, 3, ParticipantStatusMessage.value m)\n                                | NeedsEmergencySupport m   -> (2, 4, ParticipantStatusMessage.value m)\n                                | DroppedOut m              -> (2, 6, ParticipantStatusMessage.value m)\n                                | Unknown m                 -> (2, 0, ParticipantStatusMessage.value m) \n                                | _                         -> (2, 0, String.Empty)\n        | Resting m                 -> (3, 3, ParticipantStatusMessage.value m)\n        | NeedsEmergencySupport m   -> (4, 4, ParticipantStatusMessage.value m)\n        | Completed m               -> (5, 5, ParticipantStatusMessage.value m)\n        | DroppedOut m\n        | Unknown m                 -> (0, 0, ParticipantStatusMessage.value m)\n    member this.ToOptionName () =\n        match this with\n        | Continued s               -> \"Continued\"\n        | Injured s                 -> \"Injured\"\n        | Resting s                 -> \"Resting\"\n        | NeedsEmergencySupport s   -> \"Needs Emergency Support\"\n        | Completed s               -> \"Completed\"\n        | DroppedOut s              -> \"Dropped Out\"\n        | Unknown s                 -> \"Unknown\"\n    static member fromStatusCombo s =\n        match s with\n        | (1, 1, m) -> (Continued (ParticipantStatusMessage.create m))\n        | (2, 1, m) -> (Injured (Continued (ParticipantStatusMessage.create m)))\n        | (2, 3, m) -> (Injured (Resting (ParticipantStatusMessage.create m)))\n        | (2, 4, m) -> (Injured (NeedsEmergencySupport (ParticipantStatusMessage.create m)))\n        | (2, 0, m) -> (Injured (Unknown (ParticipantStatusMessage.create m)))\n        | (3, 3, m) -> (Resting (ParticipantStatusMessage.create m))\n        | (4, 4, m) -> (NeedsEmergencySupport (ParticipantStatusMessage.create m))\n        | (5, 5, m) -> (Completed (ParticipantStatusMessage.create m))\n        | (6, 6, m) -> (DroppedOut (ParticipantStatusMessage.create m))\n        | (0, 0, m) -> (Unknown (ParticipantStatusMessage.create m))\n        | (_, _, m) -> (Unknown (ParticipantStatusMessage.create m))\n```\n\n#### The final participant status report data frame\n\nThe final output for the `kissutil` will look like this:\n\n> KG7SIO>APDW15,WIDE1-1:{{P100923450004211In good shape!\n\n## Parsing received messages\n\nFAPRS can produce an APRS message, and it can parse a TNC2MON formatted frame with a number of APRS data formats. To do this I used [Active Patterns](https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/active-patterns). The parser assumes the frames were produced by the DireWolf `kissutil`, which are in this format:\n\n> [0] K1NRO-1>APDW15,WIDE2-2:!4238.80NS07105.63W#PHG5630\n\nThis is the same as what FAPRS produces for the `kissutil`, but includes the channel on which the message was received -- `[0]`.\n\n### Start by getting the frame without the channel\n\n```fsharp\n//Remove the channel from the frame\nlet (|Frame|_|) (record:string) =\n    match record with\n    | r when String.IsNullOrWhiteSpace(r) -> None\n    | r when r.IndexOf(\" \") < 1 -> None //maybe return r because there was no channel and that's ok?\n    | r when (r.Substring(r.IndexOf(\" \"))).Trim().Length > 0 -> Some ((r.Substring(r.IndexOf(\" \"))).Trim())\n    | _ -> None\n```\n\n### Get the address field -- the sender and destination\n\n```fsharp\nlet (|Address|_|) (frame:string) =\n    if frame.IndexOf(\":\") < 1 then \n        None\n    else\n        Some (frame.Substring(0, frame.IndexOf(\":\")))\n```\n\n### Get the sender and destination out of the Address\n\n```fsharp\nlet (|Sender|_|) (address:string) =\n    if address.IndexOf(\">\") < 1 then None\n    else Some (address.Substring(0, address.IndexOf(\">\")))\n\nlet (|Destination|_|) (address:string) =\n    if address.IndexOf(\">\") < 1 || address.IndexOf(\",\") < 1 then None\n    else Some (address.Substring(address.IndexOf(\">\") + 1, address.IndexOf(\",\") - address.IndexOf(\">\") - 1))\n```\n\n### Get the Path out of the Frame\n\n```fsharp\nlet (|Path|_|) (address:string) =\n    if not (address.IndexOf(\">\") = -1) && address.IndexOf(\",\") > address.IndexOf(\">\") then\n        Some (address.Substring(address.IndexOf(\",\") + 1).Split(','))\n    else\n        None\n```\n\n### Get the Message out of the Frame\n\n```fsharp\nlet (|Message|_|) (frame:string) =\n    if frame.IndexOf(\":\") < 1 then \n        None\n    else\n        Some (frame.Substring(frame.IndexOf(\":\") + 1))\n```\n\n### Parse a position report\n\n#### Get the Latitude out of the Message\n\n```fsharp\nlet (|Latitude|_|) (msg:string) = \n    let parseLatitude (posRpt:string) =\n        let lat = posRpt.Substring(1, 8)\n        match lat.EndsWith(\"N\"), lat.EndsWith(\"S\") with\n        | true, false   -> Some lat\n        | false, true   -> Some lat\n        | _             -> None\n    match getAPRSDataTypeIdentifier (msg.Substring(0,1)) with\n    | Some id   ->  match id with\n                    | PositionReportWithoutTimeStampWithMessaging   -> (parseLatitude msg)\n                    | PositionReportWithoutTimeStampNoMessaging     -> (parseLatitude msg)\n                    | _                                             -> None\n        | None     -> None //We do not have a position report and therefore no latitude\n```\n\n#### Get the Longitude out of the Message\n\n```fsharp\nlet (|Longitude|_|) (msg:string) =\n    let parseLongitude (posRpt:string) =\n        let lon = posRpt.Substring(10, 9) \n        match lon.EndsWith(\"W\"), lon.EndsWith(\"E\") with \n        | true, false   -> Some lon\n        | false, true   -> Some lon\n        | _             -> None\n        \n    match msg.Substring(9,1) with\n    | \"/\" -> parseLongitude msg\n    | _ -> None\n```\n\n#### Get the Symbol out of the Message\n\n```fsharp\nlet (|Symbol|_|) (msg:string) =\n    //TODO check that the previous char was a W or E meaning that it was probably and APRS lat/lon\n    match msg.Substring(18,1) with\n    | \"W\" -> SymbolCode.fromSymbol (msg.Substring(19,1).ToCharArray().[0]) //  getSymbolCode (msg.Substring(19,1).ToCharArray().[0])\n    | \"E\" -> SymbolCode.fromSymbol (msg.Substring(19,1).ToCharArray().[0])\n    | _ -> None\n```\n\n#### Get the Comment out of the Message\n\n```fsharp\nlet (|Comment|_|) (symbol:char) (msg:string) =\n    let comment = msg.Substring(msg.IndexOf(symbol) + 1).Trim()\n    if comment = \n        String.Empty \n    then    \n        None \n    else \n        Some comment\n```\n\n#### The Tests\n\nTo see the active patterns in action, check out `faprs.tests` `TNC2MONActivePatternsTests`.\n\nFor example\n\n```fsharp\ntestCase \"Can get message part of well formed frame with message\" <| fun _ ->\n    let result =\n        match \"[0] KG7SIO-7>APRD15,WIDE1-1,TCPXX*,qAX,CWOP-2:=03216.4N/011057.3Wb,b>,lah:blah /fishcakes\" with\n        | TNC2MonActivePatterns.Message m -> m\n        | _ -> String.Empty\n    Expect.equal result \"=03216.4N/011057.3Wb,b>,lah:blah /fishcakes\" \"Message does not match\"\n```\n\n## Sending messages and a demo (proof of concept)\n\nTo send messages we can use faprs.cli, but it only supports the `unformatted` and `position report without timestamp` APRS data formats at the moment. For the demo I am sending a position report. \n\n> See [the project's README](https://github.com/MarneeDear/FAPRS/blob/master/README.md) for more on how to run and use FAPRS\n\nI will use `dotnet run` to run the CLI command, like this:\n\n```bash\ndotnet run --project src/faprs.cli/ -- --save-to XMIT --sender KG7SIO-7 --destination KG7SIL --path WIDE1-1 --rpt latitude 32.2217 longitude -110.9265 symbol b comment \"My submission for the applied F# challenge.\"\n```\n\nThe CLI takes latitude and longitude in decimal degrees and converts it to APRS format.\n\nIn this demo I am expecting an iGate and digipeater in my area (`N7HND`) to receive my signal and forward it to [aprs.fi](https://aprs.fi), where I will be able to see my station on a map, and see the packets that were received.\n\n### Steps:\n\n1. Tune my radio to the [recommended APRS frequency](http://www.aprs.org/) `144.390` so other stations near me can pick up my signal.\n2. Attach the radio to the computer audio. I can select the \"headset\" option which lets me transmit and receive\n3. Start `DireWolf`\n4. Start the `kissutil`\n5. Enter the CLI command\n6. Check that `DireWolf` and the `kissutil` read the file that was generated and sent the message \n7. Check with aprs.fi to see if the message made it through the local digipeater\n\n![Imgur](https://i.imgur.com/QoR9wYK.gif)\n\n![Imgur](https://i.imgur.com/ksav8IY.png)\n\n## Other Resources\n\n> [![History of APRS](https://img.youtube.com/vi/OgFBXfwmKYc/0.jpg)](https://www.youtube.com/watch?v=OgFBXfwmKYc \"Everything Ham Radio Podcast\")\n\n> _click the video to watch on You Tube_\n\n### Prior Art\n\n[Python APRS Module](https://github.com/ampledata/aprs)\n\n[APRSdroid](https://aprsdroid.org/)",
      "json_metadata": "{\"tags\":[\"fsharp\",\"radio\",\"aprs\"],\"image\":[\"https://img.youtube.com/vi/8x6x_6mDVlQ/0.jpg\",\"https://img.youtube.com/vi/FJEVWMuz6Xg/0.jpg\",\"https://i.imgur.com/9w47hfD.png\",\"https://i.imgur.com/QoR9wYK.gif\",\"https://i.imgur.com/ksav8IY.png\",\"https://img.youtube.com/vi/OgFBXfwmKYc/0.jpg\"],\"links\":[\"http://foundation.fsharp.org/applied_fsharp_challenge\",\"https://github.com/MarneeDear/FAPRS\",\"https://fsharpforfunandprofit.com/posts/designing-with-types-intro/\",\"https://fsharpforfunandprofit.com/posts/convenience-active-patterns/\",\"https://fsharpforfunandprofit.com/posts/recipe-part2/\",\"http://fsprojects.github.io/Argu/\",\"https://github.com/haf/expecto\",\"https://saturnframework.org/\",\"https://youtu.be/8x6x_6mDVlQ\",\"http://www.youtube.com/watch?v=FJEVWMuz6Xg\",\"http://www.nwclimate.org/aprs/digipeater/\",\"https://www.raspberrypi.org/products/raspberry-pi-3-model-b/\",\"https://baofengtech.com/uv82\",\"https://baofengtech.com/aprs-k2-trrs-cable\",\"https://www.tapr.org/aprs_information.html\",\"http://www.aprs.org/doc/APRS101.PDF\",\"http://www.aprs.org/aprs11.html\",\"http://www.aprs.org/aprs12.html\",\"https://fsharpforfunandprofit.com/posts/designing-with-types-single-case-dus/\",\"http://aprs.org/aprs11/tocalls.txt\",\"http://wa8lmf.net/DigiPaths/\",\"https://ham.stackexchange.com/questions/6213/help-understanding-path-taken-by-aprs-packet?rq=1\",\"https://en.wikipedia.org/wiki/AX.25\",\"https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/active-patterns\",\"https://github.com/MarneeDear/FAPRS/blob/master/README.md\",\"https://aprs.fi\",\"http://www.aprs.org/\",\"https://www.youtube.com/watch?v=OgFBXfwmKYc\",\"https://github.com/ampledata/aprs\",\"https://aprsdroid.org/\"],\"app\":\"steemit/0.1\",\"format\":\"markdown\"}"
    }
  ]
}
2019/04/08 15:05:09
parent authormarnee
parent permlinkcross-platform-development-with-net-core-and-f
authorpartiko
permlinkpartiko-re-marnee-cross-platform-development-with-net-core-and-f-20190408t150508785z
title
bodyHello @marnee! This is a friendly reminder that you can **download Partiko today and start earning Steem** easier than ever before! Partiko is a fast and beautiful mobile app for Steem. You can login using your Steem account, browse, post, comment and upvote easily on your phone! You can even **earn up to 3,000 Partiko Points per day**, and easily convert them into Steem token! **Download Partiko now using the link below to receive 1000 Points as bonus right away!** https://partiko.app/referral/partiko
json metadata{"app":"partiko"}
Transaction InfoBlock #31868267/Trx 46066bbfc1341258efe7176131e216bf901f226b
View Raw JSON Data
{
  "trx_id": "46066bbfc1341258efe7176131e216bf901f226b",
  "block": 31868267,
  "trx_in_block": 29,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2019-04-08T15:05:09",
  "op": [
    "comment",
    {
      "parent_author": "marnee",
      "parent_permlink": "cross-platform-development-with-net-core-and-f",
      "author": "partiko",
      "permlink": "partiko-re-marnee-cross-platform-development-with-net-core-and-f-20190408t150508785z",
      "title": "",
      "body": "Hello @marnee! This is a friendly reminder that you can **download Partiko today and start earning Steem** easier than ever before!\n\nPartiko is a fast and beautiful mobile app for Steem. You can login using your Steem account, browse, post, comment and upvote easily on your phone!\n\nYou can even **earn up to 3,000 Partiko Points per day**, and easily convert them into Steem token!\n\n**Download Partiko now using the link below to receive 1000 Points as bonus right away!**\n\nhttps://partiko.app/referral/partiko",
      "json_metadata": "{\"app\":\"partiko\"}"
    }
  ]
}
marneereceived 0.063 SBD, 0.155 SP author reward for @marnee / cross-platform-development-with-net-core-and-f
2019/04/07 03:24:24
authormarnee
permlinkcross-platform-development-with-net-core-and-f
sbd payout0.063 SBD
steem payout0.000 STEEM
vesting payout251.884179 VESTS
Transaction InfoBlock #31825858/Virtual Operation #6
View Raw JSON Data
{
  "trx_id": "0000000000000000000000000000000000000000",
  "block": 31825858,
  "trx_in_block": 4294967295,
  "op_in_trx": 0,
  "virtual_op": 6,
  "timestamp": "2019-04-07T03:24:24",
  "op": [
    "author_reward",
    {
      "author": "marnee",
      "permlink": "cross-platform-development-with-net-core-and-f",
      "sbd_payout": "0.063 SBD",
      "steem_payout": "0.000 STEEM",
      "vesting_payout": "251.884179 VESTS"
    }
  ]
}
2019/04/05 11:59:48
voteralkasai
authormarnee
permlinkcross-platform-development-with-net-core-and-f
weight10000 (100.00%)
Transaction InfoBlock #31778593/Trx 11ee063e457f695928918f88670b3b5e65f8fb11
View Raw JSON Data
{
  "trx_id": "11ee063e457f695928918f88670b3b5e65f8fb11",
  "block": 31778593,
  "trx_in_block": 14,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2019-04-05T11:59:48",
  "op": [
    "vote",
    {
      "voter": "alkasai",
      "author": "marnee",
      "permlink": "cross-platform-development-with-net-core-and-f",
      "weight": 10000
    }
  ]
}
2019/04/02 19:39:15
voterroy2016
authormarnee
permlinkcross-platform-development-with-net-core-and-f
weight1400 (14.00%)
Transaction InfoBlock #31701420/Trx f219aea468c1364a33300ffc5c2fba8173a74c8c
View Raw JSON Data
{
  "trx_id": "f219aea468c1364a33300ffc5c2fba8173a74c8c",
  "block": 31701420,
  "trx_in_block": 27,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2019-04-02T19:39:15",
  "op": [
    "vote",
    {
      "voter": "roy2016",
      "author": "marnee",
      "permlink": "cross-platform-development-with-net-core-and-f",
      "weight": 1400
    }
  ]
}
2019/04/02 08:53:06
voterzuerich
authormarnee
permlinkcross-platform-development-with-net-core-and-f
weight6000 (60.00%)
Transaction InfoBlock #31688512/Trx eda081f7e6c8f884d50536c67a8d98979e4dcd0b
View Raw JSON Data
{
  "trx_id": "eda081f7e6c8f884d50536c67a8d98979e4dcd0b",
  "block": 31688512,
  "trx_in_block": 20,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2019-04-02T08:53:06",
  "op": [
    "vote",
    {
      "voter": "zuerich",
      "author": "marnee",
      "permlink": "cross-platform-development-with-net-core-and-f",
      "weight": 6000
    }
  ]
}
2019/04/02 08:51:24
votergoodguymate
authormarnee
permlinkcross-platform-development-with-net-core-and-f
weight10000 (100.00%)
Transaction InfoBlock #31688478/Trx 4fd45f7fc0feb2c232edab3558945299cd7433e4
View Raw JSON Data
{
  "trx_id": "4fd45f7fc0feb2c232edab3558945299cd7433e4",
  "block": 31688478,
  "trx_in_block": 30,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2019-04-02T08:51:24",
  "op": [
    "vote",
    {
      "voter": "goodguymate",
      "author": "marnee",
      "permlink": "cross-platform-development-with-net-core-and-f",
      "weight": 10000
    }
  ]
}
2019/04/02 03:57:00
parent author
parent permlinkfsharp
authormarnee
permlinkcross-platform-development-with-net-core-and-f
titleCross-platform development with .NET Core and F#
body@@ -16409,33 +16409,33 @@ cat workshop.cli -%5C +/ workshop.cli.fsp @@ -17076,35 +17076,32 @@ er.%0A%0A%60%60%60bash%0Acd -../ + workshop.test%0A%60%60 @@ -18555,16 +18555,33 @@ et run%60 - +or %60dotnet test%60 to run t @@ -19163,16 +19163,74 @@ ckies)%0A%0A +Let's see it with %60dotnet test%60%0A%0A%60%60%60bash%0Adotnet test%0A%60%60%60%0A%0A We will @@ -22604,32 +22604,36 @@ %0Adotnet sln add +src/ workshop.cli%0Adot @@ -22640,24 +22640,28 @@ net sln add +src/ workshop.tes @@ -23029,34 +23029,35 @@ F#.%0A%0A%60%60%60bash%0Acd -.. +src /workshop.domain @@ -27267,16 +27267,77 @@ ction.%0A%0A +Try to build to check for errors:%0A%0A%60%60%60bash%0Adotnet build%0A%60%60%60%0A%0A (wait fo @@ -30579,16 +30579,109 @@ t.%0A%60%60%60%0A%0A +First go to the top level folder:%0A%0ABash/Terminal%0A%60%60%60bash%0Acd ../..%0A%60%60%60%0A%0ADoS%0A%60%60%60%0Acd ..%5C..%0A%60%60%60%0A%0A Do it li @@ -34968,29 +34968,81 @@ the -Engineer code to 500 +%0A%0A%60%60%60fsharp%0AEngineering course code to 500 %0A%0A%7C Engineering -%3E 500%0A%60%60%60%0A%0A and @@ -35207,16 +35207,166 @@ ailed.%0A%0A +%60%60%60text%0AFailed Course Tests/Engineering convert to code 100%0AError Message:%0A%0AEngineering course code should be 100.%0Aexpected: 100%0A actual: 500%0A%60%60%60%0A%0A Now chan @@ -37127,32 +37127,36 @@ the CLI.%0A%0AOpen %60 - +src/ workshop.cli/Pro @@ -38461,71 +38461,67 @@ want -, but we didn't write any of that code. %0A%0A%3E Argu did it for us! +. Argu helped us write that handling in just a few lines. %0A %0A%0A!%5B @@ -40796,37 +40796,75 @@ e. %0A%0ALet's try t -hat.%0A +o run the published version.%0A%0ABaSH/Terminal %0A%60%60%60bash%0A./publi @@ -40884,24 +40884,28 @@ li.dll%0A%60%60%60%0A%0A +DoS%0A %60%60%60dos%0Apubli @@ -40920,19 +40920,19 @@ hop.cli. -exe +dll %0A%60%60%60%0A%0AWh
json metadata{"tags":["fsharp","programming","dotnet-core"],"image":["https://i.imgur.com/wZbJs1m.jpg","https://i.imgur.com/jQdT64R.jpg","https://i.imgur.com/0DvVlTt.jpg","https://i.imgur.com/7Nj8HcR.jpg","https://i.imgur.com/ZulrmEf.jpg","https://i.imgur.com/vAypKi8.jpg","https://i.imgur.com/xqp7Yoh.jpg","https://i.imgur.com/UFVza3d.jpg","https://i.imgur.com/m3pTOWn.jpg","https://i.imgur.com/y4Fsz5U.jpg","https://i.imgur.com/EwOCq50.jpg","https://i.imgur.com/VAb2LAA.jpg","https://i.imgur.com/Nwg6xRh.jpg","https://proxy.duckduckgo.com/iu/?u=http%3A%2F%2Fwww.quickmeme.com%2Fimg%2F46%2F46e64a12f117453efe8705526c25c467709cd30198aeaf592cfb76dc03d5a350.jpg&f=1"],"links":["https://itsummit.arizona.edu/interactive","https://fsprojects.github.io/Argu/","https://github.com/haf/expecto","https://safe-stack.github.io/","https://fake.build/","https://dotnet.microsoft.com/download","https://github.com/MNie/Expecto.Template","https://github.com/dotnet/templating/wiki/Available-templates-for-dotnet-new","https://docs.microsoft.com/en-us/dotnet/core/deploying/deploy-with-cli","https://docs.microsoft.com/en-us/dotnet/core/rid-catalog","https://nodejs.org/en/download/package-manager/","https://yarnpkg.com/lang/en/docs/install/#windows-stable"],"app":"steemit/0.1","format":"markdown"}
Transaction InfoBlock #31682592/Trx 95ee0f3358303b6eb6365210428fd54530ff2b0c
View Raw JSON Data
{
  "trx_id": "95ee0f3358303b6eb6365210428fd54530ff2b0c",
  "block": 31682592,
  "trx_in_block": 21,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2019-04-02T03:57:00",
  "op": [
    "comment",
    {
      "parent_author": "",
      "parent_permlink": "fsharp",
      "author": "marnee",
      "permlink": "cross-platform-development-with-net-core-and-f",
      "title": "Cross-platform development with .NET Core and F#",
      "body": "@@ -16409,33 +16409,33 @@\n cat workshop.cli\n-%5C\n+/\n workshop.cli.fsp\n@@ -17076,35 +17076,32 @@\n er.%0A%0A%60%60%60bash%0Acd \n-../\n \n+\n workshop.test%0A%60%60\n@@ -18555,16 +18555,33 @@\n et run%60 \n-\n \n+or %60dotnet test%60 \n to run t\n@@ -19163,16 +19163,74 @@\n ckies)%0A%0A\n+Let's see it with %60dotnet test%60%0A%0A%60%60%60bash%0Adotnet test%0A%60%60%60%0A%0A\n We will \n@@ -22604,32 +22604,36 @@\n %0Adotnet sln add \n+src/\n workshop.cli%0Adot\n@@ -22640,24 +22640,28 @@\n net sln add \n+src/\n workshop.tes\n@@ -23029,34 +23029,35 @@\n F#.%0A%0A%60%60%60bash%0Acd \n-..\n+src\n /workshop.domain\n@@ -27267,16 +27267,77 @@\n ction.%0A%0A\n+Try to build to check for errors:%0A%0A%60%60%60bash%0Adotnet build%0A%60%60%60%0A%0A\n (wait fo\n@@ -30579,16 +30579,109 @@\n t.%0A%60%60%60%0A%0A\n+First go to the top level folder:%0A%0ABash/Terminal%0A%60%60%60bash%0Acd ../..%0A%60%60%60%0A%0ADoS%0A%60%60%60%0Acd ..%5C..%0A%60%60%60%0A%0A\n Do it li\n@@ -34968,29 +34968,81 @@\n the \n-Engineer code to 500 \n+%0A%0A%60%60%60fsharp%0AEngineering course code to 500 %0A%0A%7C Engineering   -%3E 500%0A%60%60%60%0A%0A\n and \n@@ -35207,16 +35207,166 @@\n ailed.%0A%0A\n+%60%60%60text%0AFailed   Course Tests/Engineering convert to code 100%0AError Message:%0A%0AEngineering course code should be 100.%0Aexpected: 100%0A  actual: 500%0A%60%60%60%0A%0A\n Now chan\n@@ -37127,32 +37127,36 @@\n the CLI.%0A%0AOpen %60\n-\n \n+src/\n workshop.cli/Pro\n@@ -38461,71 +38461,67 @@\n want\n-, but we didn't write any of that code. %0A%0A%3E Argu did it for us!\n+. Argu helped us write that handling in just a few lines. %0A\n %0A%0A!%5B\n@@ -40796,37 +40796,75 @@\n e. %0A%0ALet's try t\n-hat.%0A\n+o run the published version.%0A%0ABaSH/Terminal\n %0A%60%60%60bash%0A./publi\n@@ -40884,24 +40884,28 @@\n li.dll%0A%60%60%60%0A%0A\n+DoS%0A\n %60%60%60dos%0Apubli\n@@ -40920,19 +40920,19 @@\n hop.cli.\n-exe\n+dll\n %0A%60%60%60%0A%0AWh\n",
      "json_metadata": "{\"tags\":[\"fsharp\",\"programming\",\"dotnet-core\"],\"image\":[\"https://i.imgur.com/wZbJs1m.jpg\",\"https://i.imgur.com/jQdT64R.jpg\",\"https://i.imgur.com/0DvVlTt.jpg\",\"https://i.imgur.com/7Nj8HcR.jpg\",\"https://i.imgur.com/ZulrmEf.jpg\",\"https://i.imgur.com/vAypKi8.jpg\",\"https://i.imgur.com/xqp7Yoh.jpg\",\"https://i.imgur.com/UFVza3d.jpg\",\"https://i.imgur.com/m3pTOWn.jpg\",\"https://i.imgur.com/y4Fsz5U.jpg\",\"https://i.imgur.com/EwOCq50.jpg\",\"https://i.imgur.com/VAb2LAA.jpg\",\"https://i.imgur.com/Nwg6xRh.jpg\",\"https://proxy.duckduckgo.com/iu/?u=http%3A%2F%2Fwww.quickmeme.com%2Fimg%2F46%2F46e64a12f117453efe8705526c25c467709cd30198aeaf592cfb76dc03d5a350.jpg&f=1\"],\"links\":[\"https://itsummit.arizona.edu/interactive\",\"https://fsprojects.github.io/Argu/\",\"https://github.com/haf/expecto\",\"https://safe-stack.github.io/\",\"https://fake.build/\",\"https://dotnet.microsoft.com/download\",\"https://github.com/MNie/Expecto.Template\",\"https://github.com/dotnet/templating/wiki/Available-templates-for-dotnet-new\",\"https://docs.microsoft.com/en-us/dotnet/core/deploying/deploy-with-cli\",\"https://docs.microsoft.com/en-us/dotnet/core/rid-catalog\",\"https://nodejs.org/en/download/package-manager/\",\"https://yarnpkg.com/lang/en/docs/install/#windows-stable\"],\"app\":\"steemit/0.1\",\"format\":\"markdown\"}"
    }
  ]
}
2019/03/31 03:29:51
voterbukiland
authormarnee
permlinkcross-platform-development-with-net-core-and-f
weight100 (1.00%)
Transaction InfoBlock #31624496/Trx b86c0683bda350e41ce32afcf43412f2e611e051
View Raw JSON Data
{
  "trx_id": "b86c0683bda350e41ce32afcf43412f2e611e051",
  "block": 31624496,
  "trx_in_block": 44,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2019-03-31T03:29:51",
  "op": [
    "vote",
    {
      "voter": "bukiland",
      "author": "marnee",
      "permlink": "cross-platform-development-with-net-core-and-f",
      "weight": 100
    }
  ]
}
2019/03/31 03:24:45
votersteeming-hot
authormarnee
permlinkcross-platform-development-with-net-core-and-f
weight2 (0.02%)
Transaction InfoBlock #31624394/Trx 8d667235bcc63900bc0171e21aab47d7ed6ea46e
View Raw JSON Data
{
  "trx_id": "8d667235bcc63900bc0171e21aab47d7ed6ea46e",
  "block": 31624394,
  "trx_in_block": 7,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2019-03-31T03:24:45",
  "op": [
    "vote",
    {
      "voter": "steeming-hot",
      "author": "marnee",
      "permlink": "cross-platform-development-with-net-core-and-f",
      "weight": 2
    }
  ]
}
2019/03/31 03:24:24
parent author
parent permlinkfsharp
authormarnee
permlinkcross-platform-development-with-net-core-and-f
titleCross-platform development with .NET Core and F#
bodyThis is my script for a workshop I will be leading at the [University of Arizona IT Summit InterActive day](https://itsummit.arizona.edu/interactive) # IT Summit interActive Workshop ## Workshop Leader Marnee Dearman Chief Applications Architect College of Medicine ## Summary Cross-platform software development with .NET Core and F#. This is the script I will follow but it can be used to learn or practice on your own. ![Willy Wonka](https://i.imgur.com/wZbJs1m.jpg "Ooompa loomp doopity doo.") ## Topics * .NET Core * Start a new .NET Core project from the standard templates * Start a new .NET Core project from imported templates * Create a solution file and associate projects * Adding dependencies to a project * Restoring and building a project * Running your application * Running tests * Publishing your application to different target systems (Linux, Linux ARM, MacOS, Windows) * F# * Union types and record types for elegant domain modeling and enforcing constraints * Using [Argu](https://fsprojects.github.io/Argu/) to quickly build a command-line tool * Using [Expecto](https://github.com/haf/expecto) to write tests * Using [SAFE stack](https://safe-stack.github.io/) to create web applications * Using [FAKE](https://fake.build/) to build, test, run, and deploy applications ## Workshop requirements * .NET Core 2.2 SDK * Get the latest version [here](https://dotnet.microsoft.com/download). Select your operating system and follow the installation instructions. ## Workshop setup We need a folder to work in. Let's create one. Find a file system location that works for you. I am going to do this in my Windows home directory on my Windows Subsystem for Linux environment. This is equivalent to my Windows home directory. If you are on Windows, you can use that too. Use a location that works for you. ### Ubuntu ```bash marnee@DESKTOP-BBKBQMF:/mnt/c/Users/Marnee$ pwd /mnt/c/Users/Marnee ``` ### Windows command-line: ```bash Marnee@DESKTOP-BBKBQMF C:\Users\Marnee > cd C:\Users\Marnee ``` Use the command line commands that work for your environment. ### BaSH/MacOS Terminal ```bash mkdir interactive-workshop cd interactive-workshop ``` ### DOS/Windows Command Line ```dos mkdir interactive-workshop cd interactive-workshop ``` This is the folder where we will do all of our work for the rest of the workshop. (wait for green stickies) ## .NET Core CLI The .NET Core CLI has a number of commands to help you: * Scaffold a new project from a built in template or custom project template * Add and manage packages and dependencies * Restore dependencies * Build & compile projects * Publish applications * Build and run tests * Run applications * Auto-rebuild and re-load while running (easier to make changes) ## dotnet new `dotnet new` is what we use to scaffold a new project. On your command line enter: ```bash dotnet new ``` You should see a list of options and templates. Let's look at the options: ```bash Options: -h, --help Displays help for this command. -l, --list Lists templates containing the specified name. If no name is specified, lists all templates. -n, --name The name for the output being created. If no name is specified, the name of the current directory is used. -o, --output Location to place the generated output. -i, --install Installs a source or a template pack. -u, --uninstall Uninstalls a source or a template pack. --nuget-source Specifies a NuGet source to use during install. --type Filters templates based on available types. Predefined values are "project", "item" or "other". --dry-run Displays a summary of what would happen if the given command line were run if it would result in a template creation. --force Forces content to be generated even if it would change existing files. -lang, --language Filters templates based on language and specifies the language of the template to create. ``` `--l, --list Lists templates containing the specified name. If no name is specified, lists all templates.` Let's use this option to see a list of templates. We will use templates to create our projects. Let's see what kinds of templates we have: *Your templates may look different than mine. That is ok.* ```bash dotnet new --list ``` Result ```bash Templates Short Name Language Tags ------------------------------------------------------------------------------------------------------------------------------------------------ Console Application console [C#], F#, VB Common/Console Class library classlib [C#], F#, VB Common/Library Unit Test Project mstest [C#], F#, VB Test/MSTest NUnit 3 Test Project nunit [C#], F#, VB Test/NUnit NUnit 3 Test Item nunit-test [C#], F#, VB Test/NUnit xUnit Test Project xunit [C#], F#, VB Test/xUnit Razor Page page [C#] Web/ASP.NET MVC ViewImports viewimports [C#] Web/ASP.NET MVC ViewStart viewstart [C#] Web/ASP.NET ASP.NET Core Empty web [C#], F# Web/Empty ASP.NET Core Web App (Model-View-Controller) mvc [C#], F# Web/MVC ASP.NET Core Web App webapp [C#] Web/MVC/Razor Pages ASP.NET Core with Angular angular [C#] Web/MVC/SPA ASP.NET Core with React.js react [C#] Web/MVC/SPA ASP.NET Core with React.js and Redux reactredux [C#] Web/MVC/SPA Razor Class Library razorclasslib [C#] Web/Razor/Library/Razor Class Library ASP.NET Core Web API webapi [C#], F# Web/WebAPI global.json file globaljson Config NuGet Config nugetconfig Config Web Config webconfig Config Solution File sln Solution ``` We have a lot of built-in templates. The first column is the template, the second is the short name, which is used in the `dotnet new` command, and the third is the language supported by that template. Notice we have lots of F# templates available. These are the templates we are going to start with: ```bash Templates Short Name Language Tags ------------------------------------------------------------------------------------------------------------------------------------------------ Console Application console [C#], F#, VB Common/Console Class library classlib [C#], F#, VB Common/Library Solution File sln Solution ``` ### Console Application Scaffolds a project you can use to build a CLI or an executable. We will see more later. ### Class Library This is a class library that can be referenced by other projects. It cannot be executed, or run. ### Solution File A solution file defines a set of projects that are related to each other. This is helpful in IDEs like Visual Studio Code and Visual Studio. The IDEs can use the solution file to organize your projects. The solution file can also help with the compiler. ![Business cat](https://i.imgur.com/jQdT64R.jpg "Cats run the Internet!") ## Use `dotnet new` to scaffold projects First we need a solution structure. This is what I will use and is similar to what I usually do. This mostly follows the principles of `clean architecture` or `onion architecture`. ```text interactive-workshop | workshop.sln (file) | src (folder) | workshop.cli (folder) workshop.domain (folder) workshop.test (folder) workshop.web (folder) ``` We will talk about all of these parts later, but first let's setup the folder structure. On the bash command-line this looks like this: ```bash mkdir src mkdir src/workshop.cli mkdir src/workshop.domain mkdir src/workshop.test mkdir src/workshop.web ``` (Create your folder structure now) (Wait for green stickies) ### Scaffold a new console application Let's create a `Console Application` using the F# language. It is going to live in the `workshop.cli` folder. By default, `dotnet new` names your project according to the folder in which you are creating the project. So to get a new `workshop.cli`, use `cd` to get into `src/workshop.cli`, first. ```bash cd src/workshop.cli dotnet new console -lang F# ``` A lot of stuff happened but you should see a confirmation message in the output. ```text The template "Console Application" was created successfully. ``` Great! You just created a console app. Let's see what `dotnet new` created: BaSH/Terminal ```bash ls -la ``` DoS ```bash dir ``` ```text drwxrwxrwx 1 marnee marnee 512 Mar 24 11:27 . drwxrwxrwx 1 marnee marnee 512 Mar 24 11:25 .. drwxrwxrwx 1 marnee marnee 512 Mar 24 11:27 obj -rwxrwxrwx 1 marnee marnee 172 Mar 24 11:26 Program.fs -rwxrwxrwx 1 marnee marnee 252 Mar 24 11:26 workshop.cli.fsproj ``` #### Program.fs This where all your code will go. This is the entry point for execution. #### workshop.cli.fsproj `proj` files are common to all .NET projects. This defines what files are associated with the project, and the compiler will use the `proj` file to know what code to compile. ## Run a console project Let's see what this workshop.cli will do. ### dotnet run `dotnet run` will do this by default: 1. restore dependencies 2. build (compile) the project a. This means it will turn the code into a binary file. 3. run the application Let's see what this command can do, first. ```bash dotnet run -h ``` You should see the usage and options. ```bash Usage: dotnet run [options] [[--] <additional arguments>...]] ``` ```bash Options: -h, --help Show command line help. -c, --configuration <CONFIGURATION> The configuration to run for. The default for most projects is 'Debug'. -f, --framework <FRAMEWORK> The target framework to run for. The target framework must also be specified in the project file. -p, --project The path to the project file to run (defaults to the current directory if there is only one project). --launch-profile The name of the launch profile (if any) to use when launching the application. --no-launch-profile Do not attempt to use launchSettings.json to configure the application. --no-build Do not build the project before running. Implies --no-restore. --no-restore Do not restore the project before building. -v, --verbosity <LEVEL> Set the MSBuild verbosity level. Allowed values are q[uiet], m[inimal], n[ormal], d[etailed], and diag[nostic]. --runtime <RUNTIME_IDENTIFIER> The target runtime to restore packages for. --no-dependencies Do not restore project-to-project references and only restore the specified project. --force Force all dependencies to be resolved even if the last restore was successful. This is equivalent to deleting project.assets.json. Additional Arguments: Arguments passed to the application that is being run. ``` Cool! We have a lot of options. Let's try running the default options first. Remember the usage? `dotnet run -h` can help. ```bash dotnet run ``` By default, if no `-p, --project` option passed, `dotnet` will try to find a project file in the current folder, and if it finds an executeable project, it will do the run on that application. If it seems slow, this is ok. That is because `dotnet run` is restoring and building first. If the `run` worked, you should see some friendly output: ```text Hello World from F#! ``` Great success! You just: 1. Scaffolded a console application with `dotnet new` 2. Ran the application with `dotnet run` 3. And it worked! ![demo worked](https://i.imgur.com/0DvVlTt.jpg "First step achievement unlocked") (wait for stickies) ## Class Library Let's go through the steps to start a class library. > Remember that `dotnet new` will scaffold a project with the same name as the containing folder, so remember to cd into the desired folder before running the command. Let's first go to the `workshop.domain` folder we created earlier. This is where we will scaffold our class library. ```bash cd ../workshop.domain ``` (wait for green stickies) Do you remember how to see the templates? ```bash dotnet new --list ``` The Class Library templates short name is `classlib`. Remember the usage? ```bash Examples: dotnet new mvc --auth Individual dotnet new nunit-test dotnet new --help ``` What command do we use to create a class library using F#? > The default language is C# so don't forget to specify the language if you want to use something else. ```bash dotnet new classlib -lang F# ``` Let's see what it created. BaSH/Terminal ```bash ls -la ``` DoS ```dos dir ``` You should see the new project files. ```text -rwxrwxrwx 1 marnee marnee 101 Mar 24 13:46 Library.fs drwxrwxrwx 1 marnee marnee 512 Mar 24 13:46 obj -rwxrwxrwx 1 marnee marnee 219 Mar 24 13:46 workshop.domain.fsproj ``` (wait for stickies) Here we see the `proj` file again. And a file called `Libary.fs` with a bit of code in it. Since Class Libraries are not executable, we can't run `dotnet run` on it. But we can reference it in an executable project, like a console application, and reference and use the class library in that project. > You can't access code in a separate project without referencing in. > References go in the `proj` file using certain XML elements and attributes. Let's try that. ### dotnet add First, go up one level to the `src` file. This will make it easier to write the commands. ```bash cd .. ``` Let's reference `workshop.domain` in `workshop.cli`. To do that we use the `dotnet add` command. Let's see the usage and options. ```bash dotnet add -h ``` ```text Usage: dotnet add [options] <PROJECT> [command] ``` ```text Arguments: <PROJECT> The project file to operate on. If a file is not specified, the command will search the current directory for one. ``` `<PROJECT>` is the path/project file to the project to which you want to add the reference. In this case it is the path to the `workshop.cli` project file. We will see this later. ```text Commands: package <PACKAGE_NAME> Add a NuGet package reference to the project. reference <PROJECT_PATH> Add a project-to-project reference to the project. ``` <PROJECT_PATH> is the path to the class library you want to use in your project. How would we reference `workshop.domain` from `workshop.cli`? ```bash dotnet add workshop.cli reference workshop.domain ``` > Pro tip: use tab complete. Type out a few characters and then hit tab. The command line will try to complete the path for you. This is available in both BaSH and DoS. You should see this output. ```text Reference `..\workshop.domain\workshop.domain.fsproj` added to the project. ``` Let's see what happened to the proj file. Let's print the content to the screen. BaSH/Terminal ```bash cat workshop.cli\workshop.cli.fsproj ``` DoS ```dos type workshop.cli\workshop.cli.fsproj ``` Notice this XML element: ```xml <ItemGroup> <ProjectReference Include="..\workshop.domain\workshop.domain.fsproj" /> </ItemGroup> ``` (wait for stickies) (resolve problems if any) ## Review We learned how to: * scaffold a console and class library using `dotnet new` * run a console app using `dotnet run` * add a reference to the class library using `dotnet add` (take a break and answer questions) ![review1](https://i.imgur.com/7Nj8HcR.jpg "Things are getting pretty serious") ## Scaffold a test project First, get yourself into the `workshop.test` folder. ```bash cd ../workshop.test ``` > Pro tip: tab complete is your best friend. `dotnet new` comes with templates for creating xUnit and nUnit test projects. Those are great, but let's use something `functional programming oriented.` Let's use `Expecto`. Expecto publishes a `dotnet` template that we can install and then use. Go to the [Expecto template on Github](https://github.com/MNie/Expecto.Template). > There are [lots of different templates available](https://github.com/dotnet/templating/wiki/Available-templates-for-dotnet-new). You'll see instructions on how to install the template from Nuget (Nuget is a .NET package manager and repository). ```bash dotnet new -i Expecto.Template ``` `-i` is the `option` for installing new templates. You should see output that looks like the `dotnet new -h` command. Notice in the templates list that there is a new template: ```text Expecto .net core Template expecto F# Test ``` Now we can use it to scaffold a new Expecto project. ```bash dotnet new expecto -lang F# ``` Let's see what it created. BaSH/Terminal ```bash ls -la ``` DoS ```dos dir ``` ```text -rwxrwxrwx 1 marnee marnee 123 Mar 24 20:41 Main.fs* -rwxrwxrwx 1 marnee marnee 1206 Mar 24 20:41 Sample.fs* -rwxrwxrwx 1 marnee marnee 639 Mar 24 20:41 workshop.test.fsproj* ``` Notice that we have a `proj` file and two sample test files. (wait for stickies) ### Run the tests We can use `dotnet run` to run tests because technically they are console apps with a bit of extra code scaffolded to create sample tests. Let's try it. ```bash dotnet run ``` You should see a bunch of output and stuff that looks like errors. That's ok. Some of the tests are meant to fail as demonstrations in the sample code. The most interesting bit in the last line. ```text 20:56:42 INF] EXPECTO! 8 tests run in 00:00:00.8850460 for samples – 2 passed, 1 ignored, 5 failed, 0 errored. <Expecto ``` Here we see a report of the number of tests that failed, were ignored, and passed. (wait for stickies) We will write more tests later. ![Imgur](https://i.imgur.com/ZulrmEf.jpg "100% code coverage you will have.") ## Solution file Let's create a solution file so we can tie all of our projects together. The solution provides these benefits: * `dotnet build` all of our projects at once * `dotnet test` all of you test projects at once * Visual Studio and Visual Studio Code use the solution file to organize projects Go up two levels so you are now in the `interactive-workshop` folder. ```bash cd ../.. ``` Check to make sure. ```bash pwd ``` ```dos cd ``` ### dotnet build without a solution file Let's try building (compiling) code without a solution file. ```bash dotnet build ``` What happened? A whole lotta nothing. But that's ok because we will scaffold a solution file to help us. ```text MSBUILD : error MSB1003: Specify a project or solution file. The current working directory does not contain a project or solution file. ``` Now use `dotnet new` to create the solution file. ```bash dotnet new sln ``` Let's look inside it to see what happened. ```bash ls -la ``` ```dos dir ``` Notice that `dotnet new` created a solution file with the same name as the folder. Did you see this file? ```text interactive-workshop.sln ``` Let's see what is inside. ```bash cat interactive-workshop.sln ``` ```dos type interactive-workshop.sln ``` > Pro tip: tab complete will save you time and money! That's a lot of weird stuff. It doesn't matter much but it's mostly a bunch of stuff `msbuild` and Visual Studio understand. Notice that there are no references to any of our workshop projects. That's ok. We are going to add them. But first try a `dotnet build` to see what happens. `dotnet` doesn't know what to build. That's ok. We are going to help it. (wait for stickies) ### dotnet sln add We have a new command to use that helps us with solution files. Let's see what it does. ```bash dotnet sln -h ``` ```text Commands: add <PROJECT_PATH> Add one or more projects to a solution file. list List all projects in a solution file. remove <PROJECT_PATH> Remove one or more projects from a solution file. ``` Cool! Looks like we can add projects, list projects, and remove projects. Let's add the `workshop.domain` project. ```bash dotnet sln add src/workshop.domain ``` If that worked you should be able to build now. ```bash dotnet build ``` What happened? Did it look a little like this? ```text Microsoft (R) Build Engine version 15.9.20+g88f5fadfbe for .NET Core Copyright (C) Microsoft Corporation. All rights reserved. Restoring packages for /mnt/c/Users/Marnee/interactive-workshop/src/workshop.domain/workshop.domain.fsproj... Generating MSBuild file /mnt/c/Users/Marnee/interactive-workshop/src/workshop.domain/obj/workshop.domain.fsproj.nuget.g.props. Generating MSBuild file /mnt/c/Users/Marnee/interactive-workshop/src/workshop.domain/obj/workshop.domain.fsproj.nuget.g.targets. Restore completed in 541.07 ms for /mnt/c/Users/Marnee/interactive-workshop/src/workshop.domain/workshop.domain.fsproj. workshop.domain -> /mnt/c/Users/Marnee/interactive-workshop/src/workshop.domain/bin/Debug/netstandard2.0/workshop.domain.dll Build succeeded. 0 Warning(s) 0 Error(s) Time Elapsed 00:00:11.74 ``` Great! That worked. Now try to add the `.cli` and `.test` projects. ```bash dotnet sln add workshop.cli dotnet sln add workshop.test ``` (wait for stickies) What happens when you `dotnet build` now? (wait for stickies) Awesome! This will save us time and typing effort and lessen our cognitive burden. ![solution files](https://i.imgur.com/vAypKi8.jpg "Great success!") ## Domain model and domain logic Get yourself to the `workshop.domain` folder. Let's code our domain with a little F#. ```bash cd ../workshop.domain ``` ### The domain We work at a University so let's model a course. ```text Field Type Constraints Number int 5 digits Name string 100 chars Description string 500 chars Credits int less than 4 Department int must be a valid department code ``` Ok that's good enough to get started. With your favorite editor open the Library.fs file. Let's model the `Department` first. For this we will use a `discriminated union`. It looks like this and you can think of it like an enum. ```fsharp module Workshop = type DepartmentCode = | Engineering | Geosciences | FineArts ``` The department can be for one of three departments: * Engineering * Geosciences * FineArts Each department has a department code. Let's add a way to get the department code from the DepartmentCode type. ```fsharp module Workshop = type Department = | Engineering | Geosciences | FineArts | NotFound member this.ToCode() = match this with | Engineering -> 100 | Geosciences -> 200 | FineArts -> 300 | NotFound -> 0 override this.ToString() = match this with | Engineering -> "Engineering" | Geosciences -> "Geosciences" | FineArts -> "Fine Arts" | _ -> String.Empty ``` Let's add some more. Remember the domain? ```text Field Type Constraints Number int 5 digits Name string 100 chars Description string 500 chars Credits int less than 4 Department int must be a valid department code ``` Let's create a type that models everything that makes up a course. We will use a `Record Type` to represent a course. > Pro tip: copy and paste! Don't type this all. ```fsharp type Course = { Number : int Name : CourseName Description : string Credits : int Department : Department } ``` Record types define the shape of your data. You can think of them like properties on a class. Let's make sure you don't have any syntax errors. Let's run a build. Remember how to do that? ```bash dotnet build ``` Did you get any errors? Try to fix them. I'll help. (wait for stickies) The code should currently look ike this: ```fsharp namespace workshop.domain open System module Say = let hello name = printfn "Hello %s" name module Workshop = type Department = | Engineering | Geosciences | FineArts | NotFound member this.ToCode() = match this with | Engineering -> 100 | Geosciences -> 200 | FineArts -> 300 | NotFound -> 0 override this.ToString() = match this with | Engineering -> "Engineering" | Geosciences -> "Geosciences" | FineArts -> "Fine Arts" | _ -> String.Empty let getDepartment code = match code with | 100 -> Engineering | 200 -> Geosciences | 300 -> FineArts | _ -> NotFound type Course = { Number : int Name : string Description : string Credits : int Department : Department } ``` That's nice. Let's code some constraints. Let's look at the domain again. ```text Field Type Constraints Number int 5 digits, less than 100000 Name string 100 chars ``` Let's do `Name` first. Copy and paste this above `type Course`, and I will explain it. ```fsharp type CourseName = private CourseName of string module CourseName = let create (s:string) = match s.Trim() with | nm when nm.Length <= 100 -> CourseName nm | nm -> CourseName (nm.Substring(0, 100)) let value (CourseName s) = s ``` This makes it so that you can **only** create a CourseName type things through the create function. (wait for stickies) Now that we have a `CourseName` type we can make the Name field in course that type. Like this. ```fsharp type Course = { Number : int Name : CourseName Description : string Credits : int Department : Department } ``` (wait for stickies) This means that for every instance of a Course type, you will only be able to set the Name to a value that passes the CourseName constraints. Like this. ```fsharp Name = CourseName.create "Underwater Basket Weaving" ``` Create a testCourse like this. ```fsharp let testCourse = { Number = 9999 Name = CourseName.create "Underwater Basket Weaving" Description = "Traditional basket weaving done under water for best effect." Credits = 3 Department = FineArts } ``` The whole file. ```fsharp namespace workshop.domain open System module Say = let hello name = printfn "Hello %s" name module Workshop = type Department = | Engineering | Geosciences | FineArts | NotFound member this.ToCode() = match this with | Engineering -> 100 | Geosciences -> 200 | FineArts -> 300 | NotFound -> 0 override this.ToString() = match this with | Engineering -> "Engineering" | Geosciences -> "Geosciences" | FineArts -> "Fine Arts" | _ -> String.Empty let getDepartment code = match code with | 100 -> Engineering | 200 -> Geosciences | 300 -> FineArts | _ -> NotFound type CourseName = private CourseName of string module CourseName = let create (s:string) = match s.Trim() with | nm when nm.Length <= 100 -> CourseName nm | nm -> CourseName (nm.Substring(0, 100)) let value (CourseName s) = s type Course = { Number : int Name : CourseName Description : string Credits : int Department : Department } let testCourse = { Number = 9999 Name = CourseName.create "Underwater Basket Weaving" Description = "Traditional basket weaving done under water for best effect." Credits = 3 Department = FineArts } ``` Let's see if your code builds. Do you remember how to do that? ```bash dotnet build ``` (wait for stickies) ![Remember](https://i.imgur.com/xqp7Yoh.jpg "DSL plus contraints. Make impossible states impossible and easy to read.") ## Write tests against your domain code Now that we have some code we can write tests against it. First, we will need to reference the domain project in the test project so we can use that code. > Pro tip: We can do everything by path, so we don't have to change directories. Remember the usage? ```bash Usage: dotnet add [options] <PROJECT> [command] Commands: package <PACKAGE_NAME> Add a NuGet package reference to the project. reference <PROJECT_PATH> Add a project-to-project reference to the project. ``` Do it like this. ```bash dotnet add src/workshop.test reference src/workshop.domain ``` Now let's check the `proj` file. BaSH/Terminal ```bash cat src/workshop.test/workshop.test.fsproj ``` DoS ```dos type src\workshop.test\workshop.test.fsproj ``` > Pro tip: If you aren't using tab complete then you are doing it wrong. Did you see this? ```xml <ItemGroup> <ProjectReference Include="..\workshop.domain\workshop.domain.fsproj" /> </ItemGroup> ``` (wait for stickies) Great. Now let's write a test. Create a new file and open it in your editor. I will use vim and Visual Studio Code. ```bash vim src/workshop.test/DomainTests.fs ``` ```bash code src/workshop.test/DomainTests.fs ``` Add the code: ```fsharp module DomainTests open Expecto open workshop.domain [<Tests>] let tests = testList "Course Tests" [ testCase "Engineering convert to code 100" <| fun _ -> Expect.equal (Workshop.Engineering.ToCode()) 100 "Engineering course code should be 100" ] ``` > Pro tip: You can copy and paste. Don't type it all in. Save the file. Now we need to add the file to the project file. This is so the compiler knows what to compile. > In F# the order of the files matters. The proj file specifies the order of the files. Open `workshop.test.fsproj`. Add the file in the right order. Also, let's remove the `Samples.fs` file to keep it simple. ```xml <ItemGroup> <Compile Include="DomainTests.fs" /> <Compile Include="Main.fs" /> </ItemGroup> ``` Let's check the code by building the test project. Let's take the easy way and just build the solution. ```bash dotnet build ``` (wait for stickies) If it is building, let's run the tests. ```bash dotnet test ``` Did you notice that we don't need to specify a test project? `dotnet` will iterate through each project in the solution file looking for a test project. When it finds one it will try to run the tests. ```text Build started, please wait... Skipping running test for project /mnt/c/Users/Marnee/interactive-workshop/src/workshop.cli/workshop.cli.fsproj. To run tests with dotnet test add "<IsTestProject>true<IsTestProject>" property to project file. Skipping running test for project /mnt/c/Users/Marnee/interactive-workshop/src/workshop.domain/workshop.domain.fsproj. To run tests with dotnet test add "<IsTestProject>true<IsTestProject>" property to project file. Build completed. Test run for /mnt/c/Users/Marnee/interactive-workshop/src/workshop.test/bin/Debug/netcoreapp2.2/workshop.test.dll(.NETCoreApp,Version=v2.2) Microsoft (R) Test Execution Command Line Tool Version 15.9.0 Copyright (c) Microsoft Corporation. All rights reserved. Starting test execution, please wait... ``` And then the results of the test. ```bash Total tests: 1. Passed: 1. Failed: 0. Skipped: 0. ``` * Total tests : 1 * Passed: 1 * Failed: 0 * Skipped: 0 This looks good. We had one test and it passed. Yay! (wait for stickies) ![very nice](https://i.imgur.com/UFVza3d.jpg "Automated unit tests and very nice") If we have time I'll take us through writing another test and talk about Expecto. ### dotnet watch Wouldn't it be cool if we could automatically make the tests run whenever any code changes? You can! We can use `dotnet watch` to do this. ```bash dotnet watch -h ``` ```text Examples: dotnet watch run dotnet watch test ``` The watch command. ```bash dotnet watch -p src/workshop.test test ``` The output. ```text watch : Started Build started, please wait... Build completed. Test run for /mnt/c/Users/Marnee/interactive-workshop/src/workshop.test/bin/Debug/netcoreapp2.2/workshop.test.dll(.NETCoreApp,Version=v2.2) Microsoft (R) Test Execution Command Line Tool Version 15.9.0 Copyright (c) Microsoft Corporation. All rights reserved. Starting test execution, please wait... Total tests: 1. Passed: 1. Failed: 0. Skipped: 0. Test Run Successful. Test execution time: 25.3014 Seconds watch : Exited watch : Waiting for a file to change before restarting dotnet... ``` Neat! Notice `dotnet`is patiently waiting for files to change. ```text watch : Waiting for a file to change before restarting dotnet... ``` (wait for stickies) Ok, now what would happen with the watched tests if I changed the domain model? Let's try it. In `workshop.domain` change the Engineer code to 500 and then check back in your command line. > Those of you not using a desktop, maybe you can use screen? Or just play along. I will demo. Your test should have failed. Now change the code back and see your test pass. ![autobuild](https://i.imgur.com/m3pTOWn.jpg "Build and test without using your hands!") > Stop the `dotnet watch` with `Ctl + C` or `Ctl + D` (wait for stickies) ## Build a command line tool Ok we are cooking with gas! Let's build a CLI. We are going to use a package called `Argu` that will help us quickly write a command line parser. ### Add a package reference to Argu In order to use Argu in `workshop.cli` we will need to pull in the package. First, remember how to add a dependency? ```bash dotnet add -h ``` ```text Usage: dotnet add [options] <PROJECT> [command] Commands: package <PACKAGE_NAME> Add a NuGet package reference to the project. ``` ```bash dotnet add src/workshop.cli package Argu ``` This will download the package from `nuget` and add a reference in the `proj` file. Did you see this? ```text : : : log : Installing Argu 5.2.0. info : Package 'Argu' is compatible with all the specified frameworks in project '/mnt/c/Users/Marnee/interactive-workshop/src/workshop.cli/workshop.cli.fsproj'. info : PackageReference for package 'Argu' version '5.2.0' added to file '/mnt/c/Users/Marnee/interactive-workshop/src/workshop.cli/workshop.cli.fsproj'. info : Committing restore... info : Writing lock file to disk. Path: /mnt/c/Users/Marnee/interactive-workshop/src/workshop.cli/obj/project.assets.json log : Restore completed in 7.41 sec for /mnt/c/Users/Marnee/interactive-workshop/src/workshop.cli/workshop.cli.fsproj. ``` You're the best! (wait for stickies) We also need to reference `workshop.domain` so we can use it in our CLI. Can you figure it out yourself? ```bash dotnet add src/workshop.test reference src/workshop.domain ``` Let's try to use it in the CLI. Open `workshop.cli/Program.fs`. Here is the code. ```fsharp // Learn more about F# at http://fsharp.org open System open Argu open workshop.domain //This is where we difine what options we accept on the command line type CLIArguments = | DepartmentCode of dept:int with interface IArgParserTemplate with member s.Usage = match s with | DepartmentCode _ -> "specify a course code." [<EntryPoint>] let main argv = let errorHandler = ProcessExiter(colorizer = function ErrorCode.HelpText -> None | _ -> Some ConsoleColor.Red) let parser = ArgumentParser.Create<CLIArguments>(programName = "workshop", errorHandler = errorHandler) let cmd = parser.ParseCommandLine(inputs = argv, raiseOnUsage = true) printfn "I'm doing all the things!" match cmd.TryGetResult(CLIArguments.DepartmentCode) with | Some code -> printfn "The department name is [%s]" ((Workshop.getDepartment code).ToString()) | None -> printfn "I could not understand the department code. Please see the usage." 0 // return an integer exit code ``` Let's build it to check for errors: ```bash dotnet build ``` (wait for stickies) Let's run it without building to save tme. ```bash dotnet run --no-build -p src/workshop.cli/ ``` `Ooops!` The CLI doesn't know what we want, but we didn't write any of that code. > Argu did it for us! ![testing](https://i.imgur.com/y4Fsz5U.jpg "Less boilerplate means more fun") Let's try that a different way. `dotnet` has a way to pass custom parameters to `dotnet run`. First you have your dotnet command followed by `--` followed by the parameters. What is our command usage? ```bash dotnet run --no-build -p src/workshop.cli/ -- --help ``` ```text USAGE: workshop [--help] [--departmentcode <dept>] OPTIONS: --departmentcode <dept> specify a course code. --help display this list of options. ``` Let's try passing the department code like this. ```bash dotnet run --no-build -p src/workshop.cli/ -- --departmentcode 100 ``` ![commandline](https://i.imgur.com/EwOCq50.jpg "Argu and F# FTW") If we have time I will show more how to use Argu. (wait for stickies) ## Publish your code to ... somewhere Ok let's say you are ready to publish your code. You want to share the working version with the world, but you don't want users to have to run the `dotnet` command. You want them to just use your cli. You can publish your command and all of its dependencies. You can then execute the command like you would any other program. You can even put a reference in your environment or `/usr/bin`. Whatever works for you. You can find out more in the Microsoft documentation [Deploy with CLI](https://docs.microsoft.com/en-us/dotnet/core/deploying/deploy-with-cli). Let's see the usage. ```bash dotnet publish -h ``` ```text Usage: dotnet publish [options] <PROJECT> ``` Lots of options. Let's focus on this one for right now. ```bash -o, --output <OUTPUT_DIR> The output directory to place the published artifacts in. ``` This will tell dotnet where to put your published files. Let's try that. First create a publish directory. ```bash mkdir publish ``` Let's publish. Notice the `path` I put there. `dotnet` will try to create the publish file in the same directory as the project you are publishing. If we give it the relative path it will publish there, instead. ```bash dotnet publish -o ../../publish src/workshop.cli ``` (wait for stickies) Check the publish folder contents. BaSH/Terminal ```bash ls -la publish ``` DoS ```dos dir publish ``` We have a lot of stuff in there. Let's try that. ```bash ./publish/workshop.cli.dll ``` ```dos publish\workshop.cli.exe ``` What happened? Did you get an error? ```text Unhandled Exception: System.IO.FileNotFoundException: Could not load file or assembly 'System.Runtime, Version=4.2.1.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' or one of its dependencies. The system cannot find the file specified. ``` Yep. This is because the way we published it means we need to use the dotnet command to run it. This means that if you give this to someone else to run, they will need to have dotnet installed. Let's try running it that way and then we will publish a standalone executable. ```bash dotnet publish/workshop.cli.dll ``` Did you see the output from before? Yes, because you are awesome. (wait for stickies) Having a dependency on `dotnet` isn't much fun, though. But that is ok because we can publish our cli as a stand alone such that all dependencies are `self-contained`. Can you guess what the option will be? ```bash dotnet publish -h ``` ```text --self-contained Publish the .NET Core runtime with your application so the runtime doesn't need to be installed on the target machine. The default is 'true' if a runtime identifier is specified. ``` Let's try that. ```bash dotnet publish --self-contained -o ../../publish src/workshop.cli ``` (wait for stickies) ![testing](https://i.imgur.com/VAb2LAA.jpg "The Most Interesting Man in the World uses .NET Core.") Did you get an error? ```text error NETSDK1031: It is not supported to build or publish a self-contained application without specifying a RuntimeIdentifier. Please either specify a RuntimeIdentifier or set SelfContained to false. [/mnt/c/Users/Marnee/interactive-workshop/src/workshop.cli/workshop.cli.fsproj] ``` That's ok. We need to specify a runtime identifier. This is basically the environment you want to run it on. This is the usage. ```text -r, --runtime <RUNTIME_IDENTIFIER> The target runtime to publish for. This is used when creating a self-contained deployment. The default is to publish a framework-dependent application. ``` Let's do that. Here are some common identifiers. * win10-x64 (Windows 10) * linux-x64 (Most desktop distributions like CentOS, Debian, Fedora, Ubuntu and derivatives) * osx.10.14-x64 (MacOS Mojave) Find the entire catalog [here](https://docs.microsoft.com/en-us/dotnet/core/rid-catalog) if I didn't list yours above. ```bash dotnet publish -r linux-x64 --self-contained -o ../../publish src/workshop.cli ``` No errors. Let's see what is inside the publish folder. ```bash ls -la publish ``` ```dos dir publish ``` That's a lot more stuff than we had before. Do you see ```text workshop.cli.* ``` That is your "executeable" program. (wait for stickies) What happens if we try to run it? ```bash ./publish/workshop.cli ``` That looks familiar. Let's give it a department code. ```bash ./publish/workshop.cli --departmentcode 100 ``` It works! (wait for stickies) ![testing](https://i.imgur.com/Nwg6xRh.jpg "Good Guy Greg uses .NET core") If we have time I will show how to create a web application using Saturn. ## Web Application -- SAFE STACK You'll need to install the following pre-requisites in order to build SAFE applications * `dotnet tool install -g fake-cli` * `dotnet tool install -g paket` * node.js (>= 8.0) * yarn (>= 1.10.1) or npm ### Install tools #### FAKE ```bash dotnet tool install -g fake-cli ``` #### paket (package manager) ```bash dotnet tool install -g paket ``` #### Node Find your install method [here](https://nodejs.org/en/download/package-manager/) Ubuntu ```bash curl -sL https://deb.nodesource.com/setup_11.x | sudo -E bash - sudo apt-get install -y nodejs ``` #### Yarn Find your install instructions [here](https://yarnpkg.com/lang/en/docs/install/#windows-stable). Ubuntu example ```bash curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add - echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list ``` ### Scaffold SAFE stack app #### Install the template ```bash dotnet new -i SAFE.Template ``` #### Create the project ```bash cd workshop.web ``` ```bash dotnet new SAFE -lang F# ``` ### Build and run using FAKE If you don't have a browser this might not work so good. If it works you should see a browser window or tab appear. ```bash fake build --target run ``` > If you are using WSL, if you run the app you can access it from the Windows side with this URL: ```text http://localhost:8080/ ``` And there ya go. A fully functional web application in only a handful of steps. ## Open the build script and walk through it ## The dotnet goat path ![goats](https://proxy.duckduckgo.com/iu/?u=http%3A%2F%2Fwww.quickmeme.com%2Fimg%2F46%2F46e64a12f117453efe8705526c25c467709cd30198aeaf592cfb76dc03d5a350.jpg&f=1 "Goat path to glory.") Review the templates. ```bash dotnet new --list ``` Create your folder structure. Remember to be inside the folder where you want to create the project before creating the project. > By default, dotnet new will create a project with the same name as the folder you are in. There is an option to specify the project name (`-n, --name`), which also creates the folder. I like to create the folder ahead of time so I can work out the structure first. ```bash dotnet new console -lang F# ``` This created a console app. ### Build the console app ```bash dotnet build <PATH TO CONSOLE PROJECT FOLDER> ``` ### Create a class library inside the class library folder you created. ```bash dotnet new classlib -lang F# ``` ### Build the class library. ```bash dotnet build <PATH TO LIBRARY PROJECT FOLDER> ``` ### Create a test project inside the test project folder you created. If you want to use Expecto, you need to install the templates first. ```bash dotnet new -i Expecto.Template::* ``` > There are [lots of templates out there](https://github.com/dotnet/templating/wiki/Available-templates-for-dotnet-new) for all kinds of projects. ```bash dotnet new expecto -lang F# ``` ### Run tests ```bash dotnet test <PATCH TO TEST PROJECT> ``` ### Create a solution file to help build and test without specifying a `<PATH TO PROJECT FOLDER>`. > Put the solution file in a folder above the source code folder like this. ```text sln | src workshop.cli workshop.domain workshop.test ``` ```bash dotnet new sln ``` > By default, this will create a solution file with the same name as the containing folder. You can use the option `-n` or `--name`. ### Add projects to the soution file ```bash dotnet sln add <PATH TO PROJECT FOLDER> ``` ### Build using the sln file Make sure you are in the same folder as the soltion file. `dotnet` will look for the sln file and build everything in the file. ```bash dotnet build ``` ### Run test projects using the sln file ```bash dotnet test ``` > dotnet will run any test project it finds in the solution file. ### Run a cli using dotnet with arguments ```bash dotnet run -p <PATH TO CONSOLE APP> -- --arg value ``` ### Publish self-contained app to target operating system ```bash dotnet publish -r <Runtime IDentifier> --self-contained -o <PATH TO PUBLISH FOLDER> <PATH TO CONSOLE PROJECT> ``` ### Run the published app ```bash .<PATH TO PUBLISHED EXECUTEABLE> --argu value ```
json metadata{"tags":["fsharp","programming","dotnet-core"],"image":["https://i.imgur.com/wZbJs1m.jpg","https://i.imgur.com/jQdT64R.jpg","https://i.imgur.com/0DvVlTt.jpg","https://i.imgur.com/7Nj8HcR.jpg","https://i.imgur.com/ZulrmEf.jpg","https://i.imgur.com/vAypKi8.jpg","https://i.imgur.com/xqp7Yoh.jpg","https://i.imgur.com/UFVza3d.jpg","https://i.imgur.com/m3pTOWn.jpg","https://i.imgur.com/y4Fsz5U.jpg","https://i.imgur.com/EwOCq50.jpg","https://i.imgur.com/VAb2LAA.jpg","https://i.imgur.com/Nwg6xRh.jpg","https://proxy.duckduckgo.com/iu/?u=http%3A%2F%2Fwww.quickmeme.com%2Fimg%2F46%2F46e64a12f117453efe8705526c25c467709cd30198aeaf592cfb76dc03d5a350.jpg&f=1"],"links":["https://itsummit.arizona.edu/interactive","https://fsprojects.github.io/Argu/","https://github.com/haf/expecto","https://safe-stack.github.io/","https://fake.build/","https://dotnet.microsoft.com/download","https://github.com/MNie/Expecto.Template","https://github.com/dotnet/templating/wiki/Available-templates-for-dotnet-new","https://docs.microsoft.com/en-us/dotnet/core/deploying/deploy-with-cli","https://docs.microsoft.com/en-us/dotnet/core/rid-catalog","https://nodejs.org/en/download/package-manager/","https://yarnpkg.com/lang/en/docs/install/#windows-stable"],"app":"steemit/0.1","format":"markdown"}
Transaction InfoBlock #31624387/Trx b4979599536e70d97990400554b1dce6df0b87aa
View Raw JSON Data
{
  "trx_id": "b4979599536e70d97990400554b1dce6df0b87aa",
  "block": 31624387,
  "trx_in_block": 4,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2019-03-31T03:24:24",
  "op": [
    "comment",
    {
      "parent_author": "",
      "parent_permlink": "fsharp",
      "author": "marnee",
      "permlink": "cross-platform-development-with-net-core-and-f",
      "title": "Cross-platform development with .NET Core and F#",
      "body": "This is my script for a workshop I will be leading at the [University of Arizona IT Summit InterActive day](https://itsummit.arizona.edu/interactive)\n\n# IT Summit interActive Workshop\n\n## Workshop Leader\n\nMarnee Dearman\n\nChief Applications Architect\n\nCollege of Medicine\n\n## Summary\n\nCross-platform software development with .NET Core and F#.\n\nThis is the script I will follow but it can be used to learn or practice on your own.\n\n![Willy Wonka](https://i.imgur.com/wZbJs1m.jpg \"Ooompa loomp doopity doo.\")\n\n## Topics\n\n* .NET Core\n  * Start a new .NET Core project from the standard templates\n  * Start a new .NET Core project from imported templates\n  * Create a solution file and associate projects\n  * Adding dependencies to a project\n  * Restoring and building a project\n  * Running your application\n  * Running tests\n  * Publishing your application to different target systems (Linux, Linux ARM, MacOS, Windows)\n* F#\n  * Union types and record types for elegant domain modeling and enforcing constraints\n  * Using [Argu](https://fsprojects.github.io/Argu/) to quickly build a command-line tool\n  * Using [Expecto](https://github.com/haf/expecto) to write tests\n  * Using [SAFE stack](https://safe-stack.github.io/) to create web applications\n  * Using [FAKE](https://fake.build/) to build, test, run, and deploy applications\n\n## Workshop requirements\n\n* .NET Core 2.2 SDK\n  * Get the latest version [here](https://dotnet.microsoft.com/download). Select your operating system and follow the installation instructions.\n\n## Workshop setup\n\nWe need a folder to work in. Let's create one.\n\nFind a file system location that works for you. I am going to do this in my Windows home directory on my Windows Subsystem for Linux environment. This is equivalent to my Windows home directory. If you are on Windows, you can use that too. Use a location that works for you.\n\n### Ubuntu\n\n```bash\nmarnee@DESKTOP-BBKBQMF:/mnt/c/Users/Marnee$ pwd\n/mnt/c/Users/Marnee\n```\n\n### Windows command-line:\n\n```bash\nMarnee@DESKTOP-BBKBQMF C:\\Users\\Marnee\n> cd\nC:\\Users\\Marnee\n```\n\nUse the command line commands that work for your environment.\n\n### BaSH/MacOS Terminal\n\n```bash\nmkdir interactive-workshop\ncd interactive-workshop\n```\n\n### DOS/Windows Command Line\n\n```dos\nmkdir interactive-workshop\ncd interactive-workshop\n```\n\nThis is the folder where we will do all of our work for the rest of the workshop.\n\n(wait for green stickies)\n\n## .NET Core CLI\n\nThe .NET Core CLI has a number of commands to help you:\n\n* Scaffold a new project from a built in template or custom project template\n* Add and manage packages and dependencies\n* Restore dependencies\n* Build & compile projects\n* Publish applications\n* Build and run tests\n* Run applications\n* Auto-rebuild and re-load while running (easier to make changes)\n\n## dotnet new\n\n`dotnet new` is what we use to scaffold a new project.\n\nOn your command line enter:\n\n```bash\ndotnet new\n```\n\nYou should see a list of options and templates. Let's look at the options:\n\n```bash\nOptions:\n  -h, --help          Displays help for this command.\n  -l, --list          Lists templates containing the specified name. If no name is specified, lists all templates.\n  -n, --name          The name for the output being created. If no name is specified, the name of the current directory is used.\n  -o, --output        Location to place the generated output.\n  -i, --install       Installs a source or a template pack.\n  -u, --uninstall     Uninstalls a source or a template pack.\n  --nuget-source      Specifies a NuGet source to use during install.\n  --type              Filters templates based on available types. Predefined values are \"project\", \"item\" or \"other\".\n  --dry-run           Displays a summary of what would happen if the given command line were run if it would result in a template creation.\n  --force             Forces content to be generated even if it would change existing files.\n  -lang, --language   Filters templates based on language and specifies the language of the template to create.\n```\n\n`--l, --list Lists templates containing the specified name. If no name is specified, lists all templates.`\n\nLet's use this option to see a list of templates. We will use templates to create our projects. Let's see what kinds of templates we have:\n\n*Your templates may look different than mine. That is ok.*\n\n```bash\ndotnet new --list\n```\n\nResult\n\n```bash\nTemplates                                         Short Name         Language          Tags\n------------------------------------------------------------------------------------------------------------------------------------------------\nConsole Application                               console            [C#], F#, VB      Common/Console\nClass library                                     classlib           [C#], F#, VB      Common/Library\nUnit Test Project                                 mstest             [C#], F#, VB      Test/MSTest\nNUnit 3 Test Project                              nunit              [C#], F#, VB      Test/NUnit\nNUnit 3 Test Item                                 nunit-test         [C#], F#, VB      Test/NUnit\nxUnit Test Project                                xunit              [C#], F#, VB      Test/xUnit\nRazor Page                                        page               [C#]              Web/ASP.NET\nMVC ViewImports                                   viewimports        [C#]              Web/ASP.NET\nMVC ViewStart                                     viewstart          [C#]              Web/ASP.NET\nASP.NET Core Empty                                web                [C#], F#          Web/Empty\nASP.NET Core Web App (Model-View-Controller)      mvc                [C#], F#          Web/MVC\nASP.NET Core Web App                              webapp             [C#]              Web/MVC/Razor Pages\nASP.NET Core with Angular                         angular            [C#]              Web/MVC/SPA\nASP.NET Core with React.js                        react              [C#]              Web/MVC/SPA\nASP.NET Core with React.js and Redux              reactredux         [C#]              Web/MVC/SPA\nRazor Class Library                               razorclasslib      [C#]              Web/Razor/Library/Razor Class Library\nASP.NET Core Web API                              webapi             [C#], F#          Web/WebAPI\nglobal.json file                                  globaljson                           Config\nNuGet Config                                      nugetconfig                          Config\nWeb Config                                        webconfig                            Config\nSolution File                                     sln                                  Solution\n```\n\nWe have a lot of built-in templates. \n\nThe first column is the template, the second is the short name, which is used in the `dotnet new` command, and the third is the language supported by that template. Notice we have lots of F# templates available.\n\nThese are the templates we are going to start with:\n\n```bash\nTemplates                                         Short Name         Language          Tags\n------------------------------------------------------------------------------------------------------------------------------------------------\nConsole Application                               console            [C#], F#, VB      Common/Console\nClass library                                     classlib           [C#], F#, VB      Common/Library\nSolution File                                     sln                                  Solution\n```\n\n### Console Application\n\nScaffolds a project you can use to build a CLI or an executable. We will see more later.\n\n### Class Library\n\nThis is a class library that can be referenced by other projects. It cannot be executed, or run.\n\n### Solution File\n\nA solution file defines a set of projects that are related to each other. This is helpful in IDEs like Visual Studio Code and Visual Studio. The IDEs can use the solution file to organize your projects. The solution file can also help with the compiler.\n\n![Business cat](https://i.imgur.com/jQdT64R.jpg \"Cats run the Internet!\")\n\n## Use `dotnet new` to scaffold projects\n\nFirst we need a solution structure. This is what I will use and is similar to what I usually do. This mostly follows the principles of `clean architecture` or `onion architecture`.\n\n```text\ninteractive-workshop\n                    |\n                    workshop.sln (file)\n                                        |\n                                        src (folder)\n                                                    |\n                                                    workshop.cli (folder)\n                                                    workshop.domain (folder)\n                                                    workshop.test (folder)\n                                                    workshop.web (folder)\n```\n\nWe will talk about all of these parts later, but first let's setup the folder structure.\n\nOn the bash command-line this looks like this:\n\n```bash\nmkdir src\nmkdir src/workshop.cli\nmkdir src/workshop.domain\nmkdir src/workshop.test\nmkdir src/workshop.web\n```\n\n(Create your folder structure now)\n\n(Wait for green stickies)\n\n### Scaffold a new console application\n\nLet's create a `Console Application` using the F# language. It is going to live in the `workshop.cli` folder.\n\nBy default, `dotnet new` names your project according to the folder in which you are creating the project. So to get a new `workshop.cli`, use `cd` to get into `src/workshop.cli`, first.\n\n```bash\ncd src/workshop.cli\ndotnet new console -lang F#\n```\n\nA lot of stuff happened but you should see a confirmation message in the output.\n\n```text\nThe template \"Console Application\" was created successfully.\n```\n\nGreat! You just created a console app. Let's see what `dotnet new` created:\n\nBaSH/Terminal\n\n```bash\nls -la\n```\n\nDoS\n\n```bash\ndir\n```\n\n```text\ndrwxrwxrwx 1 marnee marnee 512 Mar 24 11:27 .\ndrwxrwxrwx 1 marnee marnee 512 Mar 24 11:25 ..\ndrwxrwxrwx 1 marnee marnee 512 Mar 24 11:27 obj\n-rwxrwxrwx 1 marnee marnee 172 Mar 24 11:26 Program.fs\n-rwxrwxrwx 1 marnee marnee 252 Mar 24 11:26 workshop.cli.fsproj\n```\n\n#### Program.fs\n\nThis where all your code will go. This is the entry point for execution.\n\n#### workshop.cli.fsproj\n\n`proj` files are common to all .NET projects. This defines what files are associated with the project, and the compiler will use the `proj` file to know what code to compile.\n\n## Run a console project\n\nLet's see what this workshop.cli will do.\n\n### dotnet run\n\n`dotnet run` will do this by default:\n\n1. restore dependencies\n2. build (compile) the project\n\n    a. This means it will turn the code into a binary file.\n\n3. run the application\n\nLet's see what this command can do, first.\n\n```bash\ndotnet run -h\n```\n\nYou should see the usage and options.\n\n```bash\nUsage: dotnet run [options] [[--] <additional arguments>...]]\n```\n\n```bash\nOptions:\n  -h, --help                            Show command line help.\n  -c, --configuration <CONFIGURATION>   The configuration to run for. The default for most projects is 'Debug'.\n  -f, --framework <FRAMEWORK>           The target framework to run for. The target framework must also be specified in the project file.\n  -p, --project                         The path to the project file to run (defaults to the current directory if there is only one project).\n  --launch-profile                      The name of the launch profile (if any) to use when launching the application.\n  --no-launch-profile                   Do not attempt to use launchSettings.json to configure the application.\n  --no-build                            Do not build the project before running. Implies --no-restore.\n  --no-restore                          Do not restore the project before building.\n  -v, --verbosity <LEVEL>               Set the MSBuild verbosity level. Allowed values are q[uiet], m[inimal], n[ormal], d[etailed], and diag[nostic].\n  --runtime <RUNTIME_IDENTIFIER>        The target runtime to restore packages for.\n  --no-dependencies                     Do not restore project-to-project references and only restore the specified project.\n  --force                               Force all dependencies to be resolved even if the last restore was successful.\n                                        This is equivalent to deleting project.assets.json.\nAdditional Arguments:\n  Arguments passed to the application that is being run.\n```\n\nCool! We have a lot of options. Let's try running the default options first.\n\nRemember the usage? `dotnet run -h` can help.\n\n```bash\ndotnet run\n```\n\nBy default, if no `-p, --project` option passed, `dotnet` will try to find a project file in the current folder, and if it finds an executeable project, it will do the run on that application.\n\nIf it seems slow, this is ok. That is because `dotnet run` is restoring and building first.\n\nIf the `run` worked, you should see some friendly output:\n\n```text\nHello World from F#!\n```\n\nGreat success! You just:\n\n1. Scaffolded a console application with `dotnet new`\n2. Ran the application with `dotnet run`\n3. And it worked!\n\n![demo worked](https://i.imgur.com/0DvVlTt.jpg \"First step achievement unlocked\")\n\n(wait for stickies)\n\n## Class Library\n\nLet's go through the steps to start a class library.\n\n> Remember that `dotnet new` will scaffold a project with the same name as the containing folder, so remember to cd into the desired folder before running the command.\n\nLet's first go to the `workshop.domain` folder we created earlier. This is where we will scaffold our class library.\n\n```bash\ncd ../workshop.domain\n```\n\n(wait for green stickies)\n\nDo you remember how to see the templates?\n\n```bash\ndotnet new --list\n```\n\nThe Class Library templates short name is `classlib`.\n\nRemember the usage?\n\n```bash\nExamples:\n    dotnet new mvc --auth Individual\n    dotnet new nunit-test\n    dotnet new --help\n```\n\nWhat command do we use to create a class library using F#?\n\n> The default language is C# so don't forget to specify the language if you want to use something else.\n\n```bash\ndotnet new classlib -lang F#\n```\n\nLet's see what it created.\n\nBaSH/Terminal\n\n```bash\nls -la\n```\n\nDoS\n\n```dos\ndir\n```\n\nYou should see the new project files.\n\n```text\n-rwxrwxrwx 1 marnee marnee 101 Mar 24 13:46 Library.fs\ndrwxrwxrwx 1 marnee marnee 512 Mar 24 13:46 obj\n-rwxrwxrwx 1 marnee marnee 219 Mar 24 13:46 workshop.domain.fsproj\n```\n\n(wait for stickies)\n\nHere we see the `proj` file again. And a file called `Libary.fs` with a bit of code in it.\n\nSince Class Libraries are not executable, we can't run `dotnet run` on it. But we can reference it in an executable project, like a console application, and reference and use the class library in that project.\n\n> You can't access code in a separate project without referencing in.\n\n> References go in the `proj` file using certain XML elements and attributes.\n\nLet's try that.\n\n### dotnet add\n\nFirst, go up one level to the `src` file. This will make it easier to write the commands.\n\n```bash\ncd ..\n```\n\nLet's reference `workshop.domain` in `workshop.cli`.\n\nTo do that we use the `dotnet add` command. Let's see the usage and options.\n\n```bash\ndotnet add -h\n```\n\n```text\nUsage: dotnet add [options] <PROJECT> [command]\n```\n\n```text\nArguments:\n  <PROJECT>   The project file to operate on. If a file is not specified, the command will search the current directory for one.\n```\n\n`<PROJECT>` is the path/project file to the project to which you want to add the reference. In this case it is the path to the `workshop.cli` project file. We will see this later.\n\n```text\nCommands:\n  package <PACKAGE_NAME>     Add a NuGet package reference to the project.\n  reference <PROJECT_PATH>   Add a project-to-project reference to the project.\n```\n\n<PROJECT_PATH> is the path to the class library you want to use in your project.\n\nHow would we reference `workshop.domain` from `workshop.cli`?\n\n```bash\ndotnet add workshop.cli reference workshop.domain\n```\n\n> Pro tip: use tab complete. Type out a few characters and then hit tab. The command line will try to complete the path for you. This is available in both BaSH and DoS.\n\nYou should see this output.\n\n```text\nReference `..\\workshop.domain\\workshop.domain.fsproj` added to the project.\n```\n\nLet's see what happened to the proj file. Let's print the content to the screen.\n\nBaSH/Terminal\n\n```bash\ncat workshop.cli\\workshop.cli.fsproj\n```\n\nDoS\n\n```dos\ntype workshop.cli\\workshop.cli.fsproj\n```\n\nNotice this XML element:\n\n```xml\n<ItemGroup>\n    <ProjectReference Include=\"..\\workshop.domain\\workshop.domain.fsproj\" />\n</ItemGroup>\n```\n\n(wait for stickies)\n\n(resolve problems if any)\n\n## Review\n\nWe learned how to:\n\n* scaffold a console and class library using `dotnet new`\n* run a console app using `dotnet run`\n* add a reference to the class library using `dotnet add`\n\n(take a break and answer questions)\n\n![review1](https://i.imgur.com/7Nj8HcR.jpg \"Things are getting pretty serious\")\n\n## Scaffold a test project\n\nFirst, get yourself into the `workshop.test` folder.\n\n```bash\ncd ../workshop.test\n```\n\n> Pro tip: tab complete is your best friend.\n\n`dotnet new` comes with templates for creating xUnit and nUnit test projects. Those are great, but let's use something `functional programming oriented.` Let's use `Expecto`. \n\nExpecto publishes a `dotnet` template that we can install and then use.\n\nGo to the [Expecto template on Github](https://github.com/MNie/Expecto.Template).\n\n> There are [lots of different templates available](https://github.com/dotnet/templating/wiki/Available-templates-for-dotnet-new). \n\nYou'll see instructions on how to install the template from Nuget (Nuget is a .NET package manager and repository).\n\n```bash\ndotnet new -i Expecto.Template\n```\n\n`-i` is the `option` for installing new templates.\n\nYou should see output that looks like the `dotnet new -h` command. Notice in the templates list that there is a new template:\n\n```text\nExpecto .net core Template                        expecto            F#                Test\n```\n\nNow we can use it to scaffold a new Expecto project.\n\n```bash\ndotnet new expecto -lang F#\n```\n\nLet's see what it created.\n\nBaSH/Terminal\n\n```bash\nls -la\n```\n\nDoS\n\n```dos\ndir\n```\n\n```text\n-rwxrwxrwx 1 marnee marnee  123 Mar 24 20:41 Main.fs*\n-rwxrwxrwx 1 marnee marnee 1206 Mar 24 20:41 Sample.fs*\n-rwxrwxrwx 1 marnee marnee  639 Mar 24 20:41 workshop.test.fsproj*\n```\n\nNotice that we have a `proj` file and two sample test files.\n\n(wait for stickies)\n\n### Run the tests\n\nWe can use `dotnet run` to run tests because technically they are console apps with a  bit of extra code scaffolded to create sample tests.\n\nLet's try it.\n\n```bash\ndotnet run\n```\n\nYou should see a bunch of output and stuff that looks like errors. That's ok. Some of the tests are meant to fail as demonstrations in the sample code. The most interesting bit in the last line.\n\n```text\n20:56:42 INF] EXPECTO! 8 tests run in 00:00:00.8850460 for samples – 2 passed, 1 ignored, 5 failed, 0 errored.  <Expecto\n```\n\nHere we see a report of the number of tests that failed, were ignored, and passed.\n\n(wait for stickies)\n\nWe will write more tests later.\n\n![Imgur](https://i.imgur.com/ZulrmEf.jpg \"100% code coverage you will have.\")\n\n## Solution file\n\nLet's create a solution file so we can tie all of our projects together. The solution provides these benefits:\n\n* `dotnet build` all of our projects at once\n* `dotnet test` all of you test projects at once\n* Visual Studio and Visual Studio Code use the solution file to organize projects\n\nGo up two levels so you are now in the `interactive-workshop` folder.\n\n```bash\ncd ../..\n```\n\nCheck to make sure.\n\n```bash\npwd\n```\n\n```dos\ncd\n```\n\n### dotnet build without a solution file\n\nLet's try building (compiling) code without a solution file.\n\n```bash\ndotnet build\n```\n\nWhat happened? A whole lotta nothing. But that's ok because we will scaffold a solution file to help us.\n\n```text\nMSBUILD : error MSB1003: Specify a project or solution file. The current working directory does not contain a project or solution file.\n```\n\nNow use `dotnet new` to create the solution file.\n\n```bash\ndotnet new sln\n```\n\nLet's look inside it to see what happened.\n\n```bash\nls -la\n```\n\n```dos\ndir\n```\n\nNotice that `dotnet new` created a solution file with the same name as the folder.\n\nDid you see this file?\n\n```text\ninteractive-workshop.sln\n```\n\nLet's see what is inside.\n\n```bash\ncat interactive-workshop.sln\n```\n\n```dos\ntype interactive-workshop.sln\n```\n\n> Pro tip: tab complete will save you time and money!\n\nThat's a lot of weird stuff. It doesn't matter much but it's mostly a bunch of stuff `msbuild` and Visual Studio understand.\n\nNotice that there are no references to any of our workshop projects. That's ok. We are going to add them.\n\nBut first try a `dotnet build` to see what happens.\n\n`dotnet` doesn't know what to build. That's ok. We are going to help it.\n\n(wait for stickies)\n\n### dotnet sln add\n\nWe have a new command to use that helps us with solution files. Let's see what it does.\n\n```bash\ndotnet sln -h\n```\n\n```text\nCommands:\n  add <PROJECT_PATH>      Add one or more projects to a solution file.\n  list                    List all projects in a solution file.\n  remove <PROJECT_PATH>   Remove one or more projects from a solution file.\n```\n\nCool! Looks like we can add projects, list projects, and remove projects.\n\nLet's add the `workshop.domain` project.\n\n```bash\ndotnet sln add src/workshop.domain\n```\n\nIf that worked you should be able to build now.\n\n```bash\ndotnet build\n```\n\nWhat happened?\n\nDid it look a little like this?\n\n```text\nMicrosoft (R) Build Engine version 15.9.20+g88f5fadfbe for .NET Core\nCopyright (C) Microsoft Corporation. All rights reserved.\n\n  Restoring packages for /mnt/c/Users/Marnee/interactive-workshop/src/workshop.domain/workshop.domain.fsproj...\n  Generating MSBuild file /mnt/c/Users/Marnee/interactive-workshop/src/workshop.domain/obj/workshop.domain.fsproj.nuget.g.props.\n  Generating MSBuild file /mnt/c/Users/Marnee/interactive-workshop/src/workshop.domain/obj/workshop.domain.fsproj.nuget.g.targets.\n  Restore completed in 541.07 ms for /mnt/c/Users/Marnee/interactive-workshop/src/workshop.domain/workshop.domain.fsproj.\n  workshop.domain -> /mnt/c/Users/Marnee/interactive-workshop/src/workshop.domain/bin/Debug/netstandard2.0/workshop.domain.dll\n\nBuild succeeded.\n    0 Warning(s)\n    0 Error(s)\n\nTime Elapsed 00:00:11.74\n```\n\nGreat! That worked. Now try to add the `.cli` and `.test` projects.\n\n```bash\ndotnet sln add workshop.cli\ndotnet sln add workshop.test\n```\n\n(wait for stickies)\n\nWhat happens when you `dotnet build` now?\n\n(wait for stickies)\n\nAwesome! This will save us time and typing effort and lessen our cognitive burden.\n\n![solution files](https://i.imgur.com/vAypKi8.jpg \"Great success!\")\n\n## Domain model and domain logic\n\nGet yourself to the `workshop.domain` folder. Let's code our domain with a little F#.\n\n```bash\ncd ../workshop.domain\n```\n\n### The domain\n\nWe work at a University so let's model a course.\n\n```text\nField         Type      Constraints\n\nNumber        int       5 digits\nName          string    100 chars\nDescription   string    500 chars\nCredits       int       less than 4\nDepartment    int       must be a valid department code\n```\n\nOk that's good enough to get started.\n\nWith your favorite editor open the Library.fs file.\n\nLet's model the `Department` first. For this we will use a `discriminated union`.\n\nIt looks like this and you can think of it like an enum.\n\n```fsharp\nmodule Workshop =\n    type DepartmentCode =\n        | Engineering\n        | Geosciences\n        | FineArts\n```\n\nThe department can be for one of three departments:\n* Engineering\n* Geosciences\n* FineArts\n\nEach department has a department code. Let's add a way to get the department code from the DepartmentCode type.\n\n```fsharp\nmodule Workshop =\n    type Department = \n        | Engineering \n        | Geosciences\n        | FineArts\n        | NotFound\n        member this.ToCode() =\n            match this with\n            | Engineering   -> 100\n            | Geosciences   -> 200\n            | FineArts      -> 300\n            | NotFound      -> 0\n        override this.ToString() =\n            match this with\n            | Engineering   -> \"Engineering\"\n            | Geosciences   -> \"Geosciences\"\n            | FineArts      -> \"Fine Arts\"\n            | _             -> String.Empty\n```\n\nLet's add some more. Remember the domain?\n\n```text\nField         Type      Constraints\n\nNumber        int       5 digits\nName          string    100 chars\nDescription   string    500 chars\nCredits       int       less than 4\nDepartment    int       must be a valid department code\n```\n\nLet's create a type that models everything that makes up a course. We will use a `Record Type` to represent a course.\n\n> Pro tip: copy and paste! Don't type this all.\n\n```fsharp\n    type Course =\n        {\n            Number      : int\n            Name        : CourseName\n            Description : string\n            Credits     : int\n            Department  : Department\n        }\n```\n\nRecord types define the shape of your data. You can think of them like properties on a class.\n\nLet's make sure you don't have any syntax errors. Let's run a build. Remember how to do that?\n\n```bash\ndotnet build\n```\n\nDid you get any errors? Try to fix them. I'll help.\n\n(wait for stickies)\n\nThe code should currently look ike this:\n\n```fsharp\nnamespace workshop.domain\n\nopen System\n\nmodule Say =\n    let hello name =\n        printfn \"Hello %s\" name\n\nmodule Workshop =\n    type Department = \n        | Engineering \n        | Geosciences\n        | FineArts\n        | NotFound\n        member this.ToCode() =\n            match this with\n            | Engineering   -> 100\n            | Geosciences   -> 200\n            | FineArts      -> 300\n            | NotFound      -> 0\n        override this.ToString() =\n            match this with\n            | Engineering   -> \"Engineering\"\n            | Geosciences   -> \"Geosciences\"\n            | FineArts      -> \"Fine Arts\"\n            | _             -> String.Empty\n\n    let getDepartment code =\n        match code with\n        | 100   -> Engineering\n        | 200   -> Geosciences\n        | 300   -> FineArts\n        | _     -> NotFound\n\n    type Course =\n        {\n            Number      : int\n            Name        : string\n            Description : string\n            Credits     : int\n            Department  : Department\n        }\n```\n\nThat's nice. Let's code some constraints. Let's look at the domain again.\n\n```text\nField         Type      Constraints\n\nNumber        int       5 digits, less than 100000\nName          string    100 chars\n```\n\nLet's do `Name` first.\n\nCopy and paste this above `type Course`, and I will explain it.\n\n```fsharp\ntype CourseName = private CourseName of string\nmodule CourseName =\n    let create (s:string) =\n        match s.Trim() with\n        | nm when nm.Length <= 100  -> CourseName nm\n        | nm                        -> CourseName (nm.Substring(0, 100))\n    let value (CourseName s) = s\n```\n\nThis makes it so that you can **only** create a CourseName type things through the create function.\n\n(wait for stickies)\n\nNow that we have a `CourseName` type we can make the Name field in course that type. Like this.\n\n```fsharp\ntype Course =\n    {\n        Number      : int\n        Name        : CourseName\n        Description : string\n        Credits     : int\n        Department  : Department\n    }\n```\n\n(wait for stickies)\n\nThis means that for every instance of a Course type, you will only be able to set the Name to a value that passes the CourseName constraints. Like this.\n\n```fsharp\nName = CourseName.create \"Underwater Basket Weaving\"\n```\n\nCreate a testCourse like this.\n\n\n```fsharp\nlet testCourse =\n  {\n      Number = 9999\n      Name = CourseName.create \"Underwater Basket Weaving\"\n      Description = \"Traditional basket weaving done under water for best effect.\"\n      Credits = 3\n      Department = FineArts\n  }\n```\n\nThe whole file.\n\n```fsharp\nnamespace workshop.domain\n\nopen System\n\nmodule Say =\n    let hello name =\n        printfn \"Hello %s\" name\n\nmodule Workshop =\n    type Department = \n        | Engineering \n        | Geosciences\n        | FineArts\n        | NotFound\n        member this.ToCode() =\n            match this with\n            | Engineering   -> 100\n            | Geosciences   -> 200\n            | FineArts      -> 300\n            | NotFound      -> 0\n        override this.ToString() =\n            match this with\n            | Engineering   -> \"Engineering\"\n            | Geosciences   -> \"Geosciences\"\n            | FineArts      -> \"Fine Arts\"\n            | _             -> String.Empty\n\n    let getDepartment code =\n        match code with\n        | 100   -> Engineering\n        | 200   -> Geosciences\n        | 300   -> FineArts\n        | _     -> NotFound\n\n    type CourseName = private CourseName of string\n    module CourseName =\n        let create (s:string) =\n            match s.Trim() with\n            | nm when nm.Length <= 100  -> CourseName nm\n            | nm                        -> CourseName (nm.Substring(0, 100))\n        let value (CourseName s) = s   \n\n    type Course =\n        {\n            Number      : int\n            Name        : CourseName\n            Description : string\n            Credits     : int\n            Department  : Department\n        }\n\n    let testCourse =\n      {\n          Number = 9999\n          Name = CourseName.create \"Underwater Basket Weaving\"\n          Description = \"Traditional basket weaving done under water for best effect.\"\n          Credits = 3\n          Department = FineArts\n      }\n\n```\n\nLet's see if your code builds. Do you remember how to do that?\n\n```bash\ndotnet build\n```\n\n(wait for stickies)\n\n![Remember](https://i.imgur.com/xqp7Yoh.jpg\n \"DSL plus contraints. Make impossible states impossible and easy to read.\")\n\n\n## Write tests against your domain code\n\nNow that we have some code we can write tests against it.\n\nFirst, we will need to reference the domain project in the test project so we can use that code.\n\n> Pro tip: We can do everything by path, so we don't have to change directories. \n\nRemember the usage?\n\n```bash\nUsage: dotnet add [options] <PROJECT> [command]\nCommands:\n  package <PACKAGE_NAME>     Add a NuGet package reference to the project.\n  reference <PROJECT_PATH>   Add a project-to-project reference to the project.\n```\n\nDo it like this.\n\n```bash\ndotnet add src/workshop.test reference src/workshop.domain\n```\n\nNow let's check the `proj` file.\n\nBaSH/Terminal\n\n```bash\ncat src/workshop.test/workshop.test.fsproj\n```\n\nDoS\n\n```dos\ntype src\\workshop.test\\workshop.test.fsproj\n```\n\n> Pro tip: If you aren't using tab complete then you are doing it wrong.\n\nDid you see this?\n\n```xml\n<ItemGroup>\n    <ProjectReference Include=\"..\\workshop.domain\\workshop.domain.fsproj\" />\n</ItemGroup>\n```\n\n(wait for stickies)\n\nGreat. Now let's write a test.\n\nCreate a new file and open it in your editor. I will use vim and Visual Studio Code.\n\n```bash\nvim src/workshop.test/DomainTests.fs\n```\n\n```bash\ncode src/workshop.test/DomainTests.fs\n```\n\nAdd the code:\n\n```fsharp\nmodule DomainTests\n\nopen Expecto\nopen workshop.domain\n\n[<Tests>]\nlet tests =\n  testList \"Course Tests\" [\n      testCase \"Engineering convert to code 100\" <| fun _ ->\n        Expect.equal (Workshop.Engineering.ToCode()) 100 \"Engineering course code should be 100\"\n  ]\n\n```\n\n> Pro tip: You can copy and paste. Don't type it all in.\n\nSave the file.\n\nNow we need to add the file to the project file. This is so the compiler knows what to compile. \n\n> In F# the order of the files matters. The proj file specifies the order of the files.\n\nOpen `workshop.test.fsproj`. Add the file in the right order. Also, let's remove the `Samples.fs` file to keep it simple.\n\n```xml\n<ItemGroup>\n  <Compile Include=\"DomainTests.fs\" />\n  <Compile Include=\"Main.fs\" />\n</ItemGroup>\n```\n\nLet's check the code by building the test project. Let's take the easy way and just build the solution.\n\n```bash\ndotnet build\n```\n\n(wait for stickies)\n\nIf it is building, let's run the tests.\n\n```bash\ndotnet test\n```\n\nDid you notice that we don't need to specify a test project? `dotnet` will iterate through each project in the solution file looking for a test project. When it finds one it will try to run the tests.\n\n```text\nBuild started, please wait...\nSkipping running test for project /mnt/c/Users/Marnee/interactive-workshop/src/workshop.cli/workshop.cli.fsproj. To run tests with dotnet test add \"<IsTestProject>true<IsTestProject>\" property to project file.\nSkipping running test for project /mnt/c/Users/Marnee/interactive-workshop/src/workshop.domain/workshop.domain.fsproj. To run tests with dotnet test add \"<IsTestProject>true<IsTestProject>\" property to project file.\nBuild completed.\n\nTest run for /mnt/c/Users/Marnee/interactive-workshop/src/workshop.test/bin/Debug/netcoreapp2.2/workshop.test.dll(.NETCoreApp,Version=v2.2)\nMicrosoft (R) Test Execution Command Line Tool Version 15.9.0\nCopyright (c) Microsoft Corporation.  All rights reserved.\n\nStarting test execution, please wait...\n```\n\nAnd then the results of the test.\n\n```bash\nTotal tests: 1. Passed: 1. Failed: 0. Skipped: 0.\n```\n\n* Total tests : 1\n* Passed: 1\n* Failed: 0\n* Skipped: 0\n\nThis looks good. We had one test and it passed. Yay!\n\n(wait for stickies)\n\n![very nice](https://i.imgur.com/UFVza3d.jpg \"Automated unit tests and very nice\")\n\nIf we have time I'll take us through writing another test and talk about Expecto.\n\n### dotnet watch\n\nWouldn't it be cool if we could automatically make the tests run whenever any code changes? You can!\n\nWe can use `dotnet watch` to do this.\n\n```bash\ndotnet watch -h\n```\n\n```text\nExamples:\n  dotnet watch run\n  dotnet watch test\n```\n\nThe watch command.\n\n```bash\ndotnet watch -p src/workshop.test test\n```\n\nThe output.\n\n```text\nwatch : Started\nBuild started, please wait...\nBuild completed.\n\nTest run for /mnt/c/Users/Marnee/interactive-workshop/src/workshop.test/bin/Debug/netcoreapp2.2/workshop.test.dll(.NETCoreApp,Version=v2.2)\nMicrosoft (R) Test Execution Command Line Tool Version 15.9.0\nCopyright (c) Microsoft Corporation.  All rights reserved.\n\nStarting test execution, please wait...\n\nTotal tests: 1. Passed: 1. Failed: 0. Skipped: 0.\nTest Run Successful.\nTest execution time: 25.3014 Seconds\nwatch : Exited\nwatch : Waiting for a file to change before restarting dotnet...\n```\n\nNeat!\n\nNotice `dotnet`is patiently waiting for files to change.\n\n```text\nwatch : Waiting for a file to change before restarting dotnet...\n```\n\n(wait for stickies)\n\nOk, now what would happen with the watched tests if I changed the domain model? Let's try it.\n\nIn `workshop.domain` change the Engineer code to 500 and then check back in your command line.\n\n> Those of you not using a desktop, maybe you can use screen? Or just play along. I will demo.\n\nYour test should have failed.\n\nNow change the code back and see your test pass.\n\n![autobuild](https://i.imgur.com/m3pTOWn.jpg\n \"Build and test without using your hands!\")\n\n > Stop the `dotnet watch` with `Ctl + C` or `Ctl + D`\n\n (wait for stickies)\n\n## Build a command line tool\n\nOk we are cooking with gas! Let's build a CLI. We are going to use a package called `Argu` that will help us quickly write a command line parser.\n\n### Add a package reference to Argu\n\nIn order to use Argu in `workshop.cli` we will need to pull in the package.\n\nFirst, remember how to add a dependency?\n\n```bash\ndotnet add -h\n```\n\n```text\nUsage: dotnet add [options] <PROJECT> [command]\n\nCommands:\n  package <PACKAGE_NAME>     Add a NuGet package reference to the project.\n```\n\n```bash\ndotnet add src/workshop.cli package Argu\n```\n\nThis will download the package from `nuget` and add a reference in the `proj` file.\n\nDid you see this?\n\n```text\n:\n:\n:\n\nlog  : Installing Argu 5.2.0.\ninfo : Package 'Argu' is compatible with all the specified frameworks in project '/mnt/c/Users/Marnee/interactive-workshop/src/workshop.cli/workshop.cli.fsproj'.\ninfo : PackageReference for package 'Argu' version '5.2.0' added to file '/mnt/c/Users/Marnee/interactive-workshop/src/workshop.cli/workshop.cli.fsproj'.\ninfo : Committing restore...\ninfo : Writing lock file to disk. Path: /mnt/c/Users/Marnee/interactive-workshop/src/workshop.cli/obj/project.assets.json\nlog  : Restore completed in 7.41 sec for /mnt/c/Users/Marnee/interactive-workshop/src/workshop.cli/workshop.cli.fsproj.\n```\n\nYou're the best!\n\n(wait for stickies)\n\nWe also need to reference `workshop.domain` so we can use it in our CLI.\n\nCan you figure it out yourself?\n\n```bash\ndotnet add src/workshop.test reference src/workshop.domain\n```\n\nLet's try to use it in the CLI.\n\nOpen `workshop.cli/Program.fs`.\n\nHere is the code.\n\n```fsharp\n// Learn more about F# at http://fsharp.org\n\nopen System\nopen Argu\nopen workshop.domain\n\n//This is where we difine what options we accept on the command line\ntype CLIArguments =\n    | DepartmentCode of dept:int\nwith\n    interface IArgParserTemplate with\n        member s.Usage =\n            match s with\n            | DepartmentCode _ -> \"specify a course code.\"\n\n[<EntryPoint>]\nlet main argv =\n    let errorHandler = ProcessExiter(colorizer = function ErrorCode.HelpText -> None | _ -> Some ConsoleColor.Red)\n\n    let parser = ArgumentParser.Create<CLIArguments>(programName = \"workshop\", errorHandler = errorHandler)\n\n    let cmd = parser.ParseCommandLine(inputs = argv, raiseOnUsage = true)\n\n    printfn \"I'm doing all the things!\"\n\n    match cmd.TryGetResult(CLIArguments.DepartmentCode) with\n    | Some code -> printfn \"The department name is [%s]\" ((Workshop.getDepartment code).ToString())\n    | None      -> printfn \"I could not understand the department code. Please see the usage.\"\n\n\n    0 // return an integer exit code\n```\n\nLet's build it to check for errors:\n\n```bash\ndotnet build\n```\n\n(wait for stickies)\n\nLet's run it without building to save tme.\n\n```bash\ndotnet run --no-build -p src/workshop.cli/\n```\n\n`Ooops!` The CLI doesn't know what we want, but we didn't write any of that code. \n\n> Argu did it for us!\n\n![testing](https://i.imgur.com/y4Fsz5U.jpg\n \"Less boilerplate means more fun\")\n\nLet's try that a different way. `dotnet` has a way to pass custom parameters to `dotnet run`.\n\nFirst you have your dotnet command followed by `--` followed by the parameters.\n\nWhat is our command usage?\n\n```bash\ndotnet run --no-build -p src/workshop.cli/ -- --help\n```\n\n```text\nUSAGE: workshop [--help] [--departmentcode <dept>]\n\nOPTIONS:\n\n    --departmentcode <dept>\n                          specify a course code.\n    --help                display this list of options.\n```\n\nLet's try passing the department code like this.\n\n```bash\ndotnet run --no-build -p src/workshop.cli/ -- --departmentcode 100\n```\n\n![commandline](https://i.imgur.com/EwOCq50.jpg \"Argu and F# FTW\")\n\nIf we have time I will show more how to use Argu.\n\n(wait for stickies)\n\n## Publish your code to ... somewhere\n\nOk let's say you are ready to publish your code. You want to share the working version with the world, but you don't want users to have to run the `dotnet` command. You want them to just use your cli. You can publish your command and all of its dependencies. You can then execute the command like you would any other program. You can even put a reference in your environment or `/usr/bin`. Whatever works for you.\n\nYou can find out more in the Microsoft documentation [Deploy with CLI](https://docs.microsoft.com/en-us/dotnet/core/deploying/deploy-with-cli).\n\nLet's see the usage.\n\n```bash\ndotnet publish -h\n```\n\n```text\nUsage: dotnet publish [options] <PROJECT>\n```\n\nLots of options. Let's focus on this one for right now.\n\n```bash\n-o, --output <OUTPUT_DIR>             The output directory to place the published artifacts in.\n```\n\nThis will tell dotnet where to put your published files.\n\nLet's try that. First create a publish directory.\n\n```bash\nmkdir publish\n```\n\nLet's publish. Notice the `path` I put there. `dotnet` will try to create the publish file in the same directory as the project you are publishing. If we give it the relative path it will publish there, instead.\n\n```bash\ndotnet publish -o ../../publish src/workshop.cli\n```\n\n(wait for stickies)\n\nCheck the publish folder contents.\n\nBaSH/Terminal\n\n```bash\nls -la publish\n```\n\nDoS\n\n```dos\ndir publish\n```\n\nWe have a lot of stuff in there. \n\nLet's try that.\n\n```bash\n./publish/workshop.cli.dll\n```\n\n```dos\npublish\\workshop.cli.exe\n```\n\nWhat happened? Did you get an error?\n\n```text\nUnhandled Exception: System.IO.FileNotFoundException: Could not load file or assembly 'System.Runtime, Version=4.2.1.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' or one of its dependencies. The system cannot find the file specified.\n```\n\nYep. This is because the way we published it means we need to use the dotnet command to run it. This means that if you give this to someone else to run, they will need to have dotnet installed. Let's try running it that way and then we will publish a standalone executable.\n\n\n```bash\ndotnet publish/workshop.cli.dll\n```\n\nDid you see the output from before? Yes, because you are awesome.\n\n(wait for stickies)\n\nHaving a dependency on `dotnet` isn't much fun, though. But that is ok because we can publish our cli as a stand alone such that all dependencies are `self-contained`. Can you guess what the option will be?\n\n\n```bash\ndotnet publish -h\n```\n\n```text\n--self-contained                      Publish the .NET Core runtime with your application so the runtime doesn't need to be installed on the target machine.\n                                        The default is 'true' if a runtime identifier is specified.\n```\n\nLet's try that.\n\n```bash\ndotnet publish --self-contained -o ../../publish src/workshop.cli\n```\n\n(wait for stickies)\n\n![testing](https://i.imgur.com/VAb2LAA.jpg \"The Most Interesting Man in the World uses .NET Core.\")\n\nDid you get an error? \n\n```text\nerror NETSDK1031: It is not supported to build or publish a self-contained application without specifying a RuntimeIdentifier.  Please either specify a RuntimeIdentifier or set SelfContained to false. [/mnt/c/Users/Marnee/interactive-workshop/src/workshop.cli/workshop.cli.fsproj]\n```\n\nThat's ok. We need to specify a runtime identifier. This is basically the environment you want to run it on.\n\nThis is the usage.\n\n```text\n-r, --runtime <RUNTIME_IDENTIFIER>    The target runtime to publish for. This is used when creating a self-contained deployment.\n                                        The default is to publish a framework-dependent application.\n```\n\nLet's do that. Here are some common identifiers.\n\n* win10-x64 (Windows 10)\n* linux-x64 (Most desktop distributions like CentOS, Debian, Fedora, Ubuntu and derivatives)\n* osx.10.14-x64 (MacOS Mojave)\n\nFind the entire catalog [here](https://docs.microsoft.com/en-us/dotnet/core/rid-catalog) if I didn't list yours above.\n\n```bash\ndotnet publish -r linux-x64 --self-contained -o ../../publish src/workshop.cli\n```\n\nNo errors. Let's see what is inside the publish folder.\n\n```bash\nls -la publish\n```\n\n```dos\ndir publish\n```\n\nThat's a lot more stuff than we had before. Do you see\n\n```text\nworkshop.cli.*\n```\n\nThat is your \"executeable\" program.\n\n(wait for stickies)\n\nWhat happens if we try to run it?\n\n```bash\n ./publish/workshop.cli\n```\n\nThat looks familiar. Let's give it a department code.\n\n```bash\n ./publish/workshop.cli --departmentcode 100\n```\n\n It works!\n\n(wait for stickies)\n\n\n\n![testing](https://i.imgur.com/Nwg6xRh.jpg \"Good Guy Greg uses .NET core\")\n\nIf we have time I will show how to create a web application using Saturn.\n\n## Web Application -- SAFE STACK\n\nYou'll need to install the following pre-requisites in order to build SAFE applications\n\n* `dotnet tool install -g fake-cli`\n* `dotnet tool install -g paket`\n* node.js (>= 8.0)\n* yarn (>= 1.10.1) or npm\n\n### Install tools\n\n#### FAKE\n\n```bash\ndotnet tool install -g fake-cli\n```\n\n#### paket (package manager)\n\n```bash\ndotnet tool install -g paket\n```\n\n#### Node\n\nFind your install method [here](https://nodejs.org/en/download/package-manager/)\n\nUbuntu\n```bash\ncurl -sL https://deb.nodesource.com/setup_11.x | sudo -E bash -\nsudo apt-get install -y nodejs\n```\n\n#### Yarn\n\nFind your install instructions [here](https://yarnpkg.com/lang/en/docs/install/#windows-stable).\n\nUbuntu example\n```bash\ncurl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -\necho \"deb https://dl.yarnpkg.com/debian/ stable main\" | sudo tee /etc/apt/sources.list.d/yarn.list\n```\n\n### Scaffold SAFE stack app \n\n#### Install the template\n\n```bash\ndotnet new -i SAFE.Template\n```\n\n#### Create the project\n\n```bash\ncd workshop.web\n```\n\n```bash\ndotnet new SAFE -lang F#\n```\n\n### Build and run using FAKE\n\nIf you don't have a browser this might not work so good. If it works you should see a browser window or tab appear.\n\n```bash\nfake build --target run\n```\n\n> If you are using WSL, if you run the app you can access it from the Windows side with this URL:\n\n```text\nhttp://localhost:8080/\n```\n\nAnd there ya go. A fully functional web application in only a handful of steps.\n\n\n## Open the build script and walk through it\n\n## The dotnet goat path\n\n![goats](https://proxy.duckduckgo.com/iu/?u=http%3A%2F%2Fwww.quickmeme.com%2Fimg%2F46%2F46e64a12f117453efe8705526c25c467709cd30198aeaf592cfb76dc03d5a350.jpg&f=1 \"Goat path to glory.\")\n\nReview the templates.\n\n```bash\ndotnet new --list\n```\n\nCreate your folder structure. Remember to be inside the folder where you want to create the project before creating the project.\n\n> By default, dotnet new will create a project with the same name as the folder you are in. There is an option to specify the project name (`-n, --name`), which also creates the folder. I like to create the folder ahead of time so I can work out the structure first.\n\n```bash\ndotnet new console -lang F#\n```\n\nThis created a console app.\n\n### Build the console app\n\n```bash\ndotnet build <PATH TO CONSOLE PROJECT FOLDER>\n```\n\n### Create a class library inside the class library folder you created.\n\n```bash\ndotnet new classlib -lang F#\n```\n\n### Build the class library.\n\n```bash\ndotnet build <PATH TO LIBRARY PROJECT FOLDER>\n```\n\n### Create a test project inside the test project folder you created.\n\nIf you want to use Expecto, you need to install the templates first.\n\n```bash\ndotnet new -i Expecto.Template::*\n```\n\n> There are [lots of templates out there](https://github.com/dotnet/templating/wiki/Available-templates-for-dotnet-new) for all kinds of projects. \n\n```bash\ndotnet new expecto -lang F#\n```\n\n### Run tests\n\n```bash\ndotnet test <PATCH TO TEST PROJECT>\n```\n\n### Create a solution file to help build and test without specifying a `<PATH TO PROJECT FOLDER>`.\n\n> Put the solution file in a folder above the source code folder like this.\n\n```text\nsln\n    |\n    src\n        workshop.cli\n        workshop.domain\n        workshop.test\n```\n\n```bash\ndotnet new sln\n```\n\n> By default, this will create a solution file with the same name as the containing folder. You can use the option `-n` or `--name`.\n\n### Add projects to the soution file\n\n```bash\ndotnet sln add <PATH TO PROJECT FOLDER>\n```\n\n### Build using the sln file\n\nMake sure you are in the same folder as the soltion file. `dotnet` will look for the sln file and build everything in the file.\n\n```bash\ndotnet build\n```\n\n### Run test projects using the sln file\n\n```bash\ndotnet test\n```\n\n> dotnet will run any test project it finds in the solution file.\n\n### Run a cli using dotnet with arguments\n\n```bash\ndotnet run -p <PATH TO CONSOLE APP> -- --arg value\n```\n\n### Publish self-contained app to target operating system\n\n```bash\ndotnet publish -r <Runtime IDentifier> --self-contained -o <PATH TO PUBLISH FOLDER> <PATH TO CONSOLE PROJECT>\n```\n\n### Run the published app\n\n```bash\n.<PATH TO PUBLISHED EXECUTEABLE> --argu value\n```",
      "json_metadata": "{\"tags\":[\"fsharp\",\"programming\",\"dotnet-core\"],\"image\":[\"https://i.imgur.com/wZbJs1m.jpg\",\"https://i.imgur.com/jQdT64R.jpg\",\"https://i.imgur.com/0DvVlTt.jpg\",\"https://i.imgur.com/7Nj8HcR.jpg\",\"https://i.imgur.com/ZulrmEf.jpg\",\"https://i.imgur.com/vAypKi8.jpg\",\"https://i.imgur.com/xqp7Yoh.jpg\",\"https://i.imgur.com/UFVza3d.jpg\",\"https://i.imgur.com/m3pTOWn.jpg\",\"https://i.imgur.com/y4Fsz5U.jpg\",\"https://i.imgur.com/EwOCq50.jpg\",\"https://i.imgur.com/VAb2LAA.jpg\",\"https://i.imgur.com/Nwg6xRh.jpg\",\"https://proxy.duckduckgo.com/iu/?u=http%3A%2F%2Fwww.quickmeme.com%2Fimg%2F46%2F46e64a12f117453efe8705526c25c467709cd30198aeaf592cfb76dc03d5a350.jpg&f=1\"],\"links\":[\"https://itsummit.arizona.edu/interactive\",\"https://fsprojects.github.io/Argu/\",\"https://github.com/haf/expecto\",\"https://safe-stack.github.io/\",\"https://fake.build/\",\"https://dotnet.microsoft.com/download\",\"https://github.com/MNie/Expecto.Template\",\"https://github.com/dotnet/templating/wiki/Available-templates-for-dotnet-new\",\"https://docs.microsoft.com/en-us/dotnet/core/deploying/deploy-with-cli\",\"https://docs.microsoft.com/en-us/dotnet/core/rid-catalog\",\"https://nodejs.org/en/download/package-manager/\",\"https://yarnpkg.com/lang/en/docs/install/#windows-stable\"],\"app\":\"steemit/0.1\",\"format\":\"markdown\"}"
    }
  ]
}
steemdelegated 17.788 SP to @marnee
2019/03/31 01:42:21
delegatorsteem
delegateemarnee
vesting shares28965.915848 VESTS
Transaction InfoBlock #31622346/Trx 1b283389694a4827d987b22813ad42778a8a5cf4
View Raw JSON Data
{
  "trx_id": "1b283389694a4827d987b22813ad42778a8a5cf4",
  "block": 31622346,
  "trx_in_block": 15,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2019-03-31T01:42:21",
  "op": [
    "delegate_vesting_shares",
    {
      "delegator": "steem",
      "delegatee": "marnee",
      "vesting_shares": "28965.915848 VESTS"
    }
  ]
}
2019/03/31 01:06:45
votermarnee
authorcareywedler
permlinkthe-internet-just-crowdfunded-the-release-of-4-358-cia-mind-control-documents
weight10000 (100.00%)
Transaction InfoBlock #31621635/Trx 6bed851ae1eed7a09ff8c17a44dcb68e3c1896cd
View Raw JSON Data
{
  "trx_id": "6bed851ae1eed7a09ff8c17a44dcb68e3c1896cd",
  "block": 31621635,
  "trx_in_block": 16,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2019-03-31T01:06:45",
  "op": [
    "vote",
    {
      "voter": "marnee",
      "author": "careywedler",
      "permlink": "the-internet-just-crowdfunded-the-release-of-4-358-cia-mind-control-documents",
      "weight": 10000
    }
  ]
}
marneeupvoted (100.00%) @careywedler / r39oin8b
2019/03/31 00:58:45
votermarnee
authorcareywedler
permlinkr39oin8b
weight10000 (100.00%)
Transaction InfoBlock #31621475/Trx 47f531b56242d99081f39935071d54d906160c07
View Raw JSON Data
{
  "trx_id": "47f531b56242d99081f39935071d54d906160c07",
  "block": 31621475,
  "trx_in_block": 25,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2019-03-31T00:58:45",
  "op": [
    "vote",
    {
      "voter": "marnee",
      "author": "careywedler",
      "permlink": "r39oin8b",
      "weight": 10000
    }
  ]
}
2019/03/31 00:57:15
required auths[]
required posting auths["marnee"]
idfollow
json["follow",{"follower":"marnee","following":"careywedler","what":["blog"]}]
Transaction InfoBlock #31621446/Trx 19f4fbc6ad8f648c959ff5e03d8eba79c8b4f6b2
View Raw JSON Data
{
  "trx_id": "19f4fbc6ad8f648c959ff5e03d8eba79c8b4f6b2",
  "block": 31621446,
  "trx_in_block": 57,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2019-03-31T00:57:15",
  "op": [
    "custom_json",
    {
      "required_auths": [],
      "required_posting_auths": [
        "marnee"
      ],
      "id": "follow",
      "json": "[\"follow\",{\"follower\":\"marnee\",\"following\":\"careywedler\",\"what\":[\"blog\"]}]"
    }
  ]
}
2019/03/31 00:43:54
votermarnee
authornathan-rokus
permlinkblockchain-technology-and-functional-programming-the-perfect-marriage
weight10000 (100.00%)
Transaction InfoBlock #31621179/Trx f96c1f746cfa3d705baa3117c81a06c9fdc7a5a4
View Raw JSON Data
{
  "trx_id": "f96c1f746cfa3d705baa3117c81a06c9fdc7a5a4",
  "block": 31621179,
  "trx_in_block": 11,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2019-03-31T00:43:54",
  "op": [
    "vote",
    {
      "voter": "marnee",
      "author": "nathan-rokus",
      "permlink": "blockchain-technology-and-functional-programming-the-perfect-marriage",
      "weight": 10000
    }
  ]
}
2019/03/31 00:25:51
votermarnee
authorwoz.software
permlinkre-nphacker-why-functional-languages-should-be-used-for-blockchain-development-20170710t063608723z
weight10000 (100.00%)
Transaction InfoBlock #31620818/Trx f331150a5779655e818b9bb8441d9ec97266f104
View Raw JSON Data
{
  "trx_id": "f331150a5779655e818b9bb8441d9ec97266f104",
  "block": 31620818,
  "trx_in_block": 28,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2019-03-31T00:25:51",
  "op": [
    "vote",
    {
      "voter": "marnee",
      "author": "woz.software",
      "permlink": "re-nphacker-why-functional-languages-should-be-used-for-blockchain-development-20170710t063608723z",
      "weight": 10000
    }
  ]
}
2019/03/31 00:25:33
votermarnee
authornphacker
permlinkwhy-functional-languages-should-be-used-for-blockchain-development
weight10000 (100.00%)
Transaction InfoBlock #31620812/Trx 55f07956a9d7bcc539a9d033b60ae90630f6438e
View Raw JSON Data
{
  "trx_id": "55f07956a9d7bcc539a9d033b60ae90630f6438e",
  "block": 31620812,
  "trx_in_block": 38,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2019-03-31T00:25:33",
  "op": [
    "vote",
    {
      "voter": "marnee",
      "author": "nphacker",
      "permlink": "why-functional-languages-should-be-used-for-blockchain-development",
      "weight": 10000
    }
  ]
}
2019/03/31 00:14:42
votermarnee
authorfredkese
permlinkhow-are-you-i-am-fine
weight10000 (100.00%)
Transaction InfoBlock #31620595/Trx df56cc25f67e0d24481bb5dc0a121127ebc312b9
View Raw JSON Data
{
  "trx_id": "df56cc25f67e0d24481bb5dc0a121127ebc312b9",
  "block": 31620595,
  "trx_in_block": 36,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2019-03-31T00:14:42",
  "op": [
    "vote",
    {
      "voter": "marnee",
      "author": "fredkese",
      "permlink": "how-are-you-i-am-fine",
      "weight": 10000
    }
  ]
}
steemdelegated 5.520 SP to @marnee
2019/02/22 23:27:00
delegatorsteem
delegateemarnee
vesting shares8988.042078 VESTS
Transaction InfoBlock #30583536/Trx 0b8e41d6378771284cfe49cd3bc7167fad482c60
View Raw JSON Data
{
  "trx_id": "0b8e41d6378771284cfe49cd3bc7167fad482c60",
  "block": 30583536,
  "trx_in_block": 5,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2019-02-22T23:27:00",
  "op": [
    "delegate_vesting_shares",
    {
      "delegator": "steem",
      "delegatee": "marnee",
      "vesting_shares": "8988.042078 VESTS"
    }
  ]
}
steemdelegated 17.830 SP to @marnee
2019/02/19 12:01:57
delegatorsteem
delegateemarnee
vesting shares29033.910633 VESTS
Transaction InfoBlock #30483498/Trx 623d304702b1afa81e9c2a3915f13b59c5384c91
View Raw JSON Data
{
  "trx_id": "623d304702b1afa81e9c2a3915f13b59c5384c91",
  "block": 30483498,
  "trx_in_block": 17,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2019-02-19T12:01:57",
  "op": [
    "delegate_vesting_shares",
    {
      "delegator": "steem",
      "delegatee": "marnee",
      "vesting_shares": "29033.910633 VESTS"
    }
  ]
}
2018/11/24 01:51:39
votermarnee
authormagpielover
permlinkopen-platform-stripe-for-cryptocurrencies
weight10000 (100.00%)
Transaction InfoBlock #27967626/Trx 47937eeeeb79908039a5df25b9c45f7c6d0dd502
View Raw JSON Data
{
  "trx_id": "47937eeeeb79908039a5df25b9c45f7c6d0dd502",
  "block": 27967626,
  "trx_in_block": 9,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2018-11-24T01:51:39",
  "op": [
    "vote",
    {
      "voter": "marnee",
      "author": "magpielover",
      "permlink": "open-platform-stripe-for-cryptocurrencies",
      "weight": 10000
    }
  ]
}
2018/11/24 01:51:12
votermarnee
authormagpielover
permlinkusechain-an-identity-centric-blockchain
weight10000 (100.00%)
Transaction InfoBlock #27967617/Trx 7128541675e339c47f0192fedc8cf34b6979fc1c
View Raw JSON Data
{
  "trx_id": "7128541675e339c47f0192fedc8cf34b6979fc1c",
  "block": 27967617,
  "trx_in_block": 16,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2018-11-24T01:51:12",
  "op": [
    "vote",
    {
      "voter": "marnee",
      "author": "magpielover",
      "permlink": "usechain-an-identity-centric-blockchain",
      "weight": 10000
    }
  ]
}
2018/11/24 01:50:03
required auths[]
required posting auths["marnee"]
idfollow
json["follow",{"follower":"marnee","following":"magpielover","what":["blog"]}]
Transaction InfoBlock #27967594/Trx e48acfd465203baf5da47f3c7cf65901795660ec
View Raw JSON Data
{
  "trx_id": "e48acfd465203baf5da47f3c7cf65901795660ec",
  "block": 27967594,
  "trx_in_block": 18,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2018-11-24T01:50:03",
  "op": [
    "custom_json",
    {
      "required_auths": [],
      "required_posting_auths": [
        "marnee"
      ],
      "id": "follow",
      "json": "[\"follow\",{\"follower\":\"marnee\",\"following\":\"magpielover\",\"what\":[\"blog\"]}]"
    }
  ]
}
2018/11/23 22:32:09
votermagpielover
authormarnee
permlinksaturn-with-cas-single-sign-on-sample-application
weight10000 (100.00%)
Transaction InfoBlock #27963643/Trx 3500059945b63f1e4ec04f7ce0c8718f55d51064
View Raw JSON Data
{
  "trx_id": "3500059945b63f1e4ec04f7ce0c8718f55d51064",
  "block": 27963643,
  "trx_in_block": 0,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2018-11-23T22:32:09",
  "op": [
    "vote",
    {
      "voter": "magpielover",
      "author": "marnee",
      "permlink": "saturn-with-cas-single-sign-on-sample-application",
      "weight": 10000
    }
  ]
}
2018/11/23 22:14:30
parent author
parent permlinkfsharp
authormarnee
permlinksaturn-with-cas-single-sign-on-sample-application
titleSaturn with CAS Single Sign-On Sample Application
body@@ -1,57 +1,4 @@ -# Saturn with CAS Single Sign-On Sample Application%0A%0A Less
json metadata{"tags":["fsharp","functional","programming","aspnet","cas"],"image":["https://saturnframework.org/assets/img/logo.png","http://www.vitamin-ha.com/wp-content/uploads/2013/05/Now-What-Meme.jpg","https://i.gr-assets.com/images/S/compressed.photo.goodreads.com/hostedimages/1381270478i/4622285.jpg"],"links":["https://github.com/MarneeDear/saturn-example","https://medicine.arizona.edu/","https://github.com/giraffe-fsharp/Giraffe","https://saturnframework.org/","https://github.com/SaturnFramework/Saturn/tree/master/sample","https://www.youtube.com/watch?v=bYor0oBgvws","https://saturnframework.org/docs/api/pipeline/","https://saturnframework.org/docs/guides/how-to-start/","https://www.microsoft.com/net/download","https://en.wikipedia.org/wiki/Central_Authentication_Service","https://www.nuget.org/packages/AspNetCore.Security.CAS","https://github.com/iuCrimson/aspnet.security.cas","https://www.c-sharpcorner.com/UploadFile/dhananjaycoder/step-by-step-implementing-onion-architecture-in-Asp-Net-mvc/","https://xunit.github.io/","https://fsprojects.github.io/FsUnit/xUnit.html","https://saturnframework.org/docs/api/application/#application-builder","https://github.com/MarneeDear/saturn-example/blob/master/src/Template.Saturn.WebHost/Program.fs","https://github.com/MarneeDear/saturn-example/blob/master/src/Template.Saturn.WebHost/Authentication/Cas.fs","https://github.com/SaturnFramework/Saturn/blob/master/src/Saturn/Application.fs","https://docs.microsoft.com/tr-tr/dotnet/api/microsoft.aspnetcore.builder.authappbuilderextensions.useauthentication?view=aspnetcore-2.0#Microsoft_AspNetCore_Builder_AuthAppBuilderExtensions_UseAuthentication_Microsoft_AspNetCore_Builder_IApplicationBuilder_","https://saturnframework.org/docs/api/scope/","https://docs.microsoft.com/en-us/aspnet/mvc/overview/older-versions-1/controllers-and-routing/asp-net-mvc-routing-overview-cs","https://docs.microsoft.com/en-us/aspnet/core/security/authentication/identity?view=aspnetcore-2.1&tabs=visual-studio","https://stackoverflow.com/questions/10848086/authorize-attribute-in-asp-net-mvc#10848142"],"app":"steemit/0.1","format":"markdown"}
Transaction InfoBlock #27963290/Trx 419a880401dbc71d5ebff0d82ddc93b57513fdbf
View Raw JSON Data
{
  "trx_id": "419a880401dbc71d5ebff0d82ddc93b57513fdbf",
  "block": 27963290,
  "trx_in_block": 11,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2018-11-23T22:14:30",
  "op": [
    "comment",
    {
      "parent_author": "",
      "parent_permlink": "fsharp",
      "author": "marnee",
      "permlink": "saturn-with-cas-single-sign-on-sample-application",
      "title": "Saturn with CAS Single Sign-On Sample Application",
      "body": "@@ -1,57 +1,4 @@\n-# Saturn with CAS Single Sign-On Sample Application%0A%0A\n Less\n",
      "json_metadata": "{\"tags\":[\"fsharp\",\"functional\",\"programming\",\"aspnet\",\"cas\"],\"image\":[\"https://saturnframework.org/assets/img/logo.png\",\"http://www.vitamin-ha.com/wp-content/uploads/2013/05/Now-What-Meme.jpg\",\"https://i.gr-assets.com/images/S/compressed.photo.goodreads.com/hostedimages/1381270478i/4622285.jpg\"],\"links\":[\"https://github.com/MarneeDear/saturn-example\",\"https://medicine.arizona.edu/\",\"https://github.com/giraffe-fsharp/Giraffe\",\"https://saturnframework.org/\",\"https://github.com/SaturnFramework/Saturn/tree/master/sample\",\"https://www.youtube.com/watch?v=bYor0oBgvws\",\"https://saturnframework.org/docs/api/pipeline/\",\"https://saturnframework.org/docs/guides/how-to-start/\",\"https://www.microsoft.com/net/download\",\"https://en.wikipedia.org/wiki/Central_Authentication_Service\",\"https://www.nuget.org/packages/AspNetCore.Security.CAS\",\"https://github.com/iuCrimson/aspnet.security.cas\",\"https://www.c-sharpcorner.com/UploadFile/dhananjaycoder/step-by-step-implementing-onion-architecture-in-Asp-Net-mvc/\",\"https://xunit.github.io/\",\"https://fsprojects.github.io/FsUnit/xUnit.html\",\"https://saturnframework.org/docs/api/application/#application-builder\",\"https://github.com/MarneeDear/saturn-example/blob/master/src/Template.Saturn.WebHost/Program.fs\",\"https://github.com/MarneeDear/saturn-example/blob/master/src/Template.Saturn.WebHost/Authentication/Cas.fs\",\"https://github.com/SaturnFramework/Saturn/blob/master/src/Saturn/Application.fs\",\"https://docs.microsoft.com/tr-tr/dotnet/api/microsoft.aspnetcore.builder.authappbuilderextensions.useauthentication?view=aspnetcore-2.0#Microsoft_AspNetCore_Builder_AuthAppBuilderExtensions_UseAuthentication_Microsoft_AspNetCore_Builder_IApplicationBuilder_\",\"https://saturnframework.org/docs/api/scope/\",\"https://docs.microsoft.com/en-us/aspnet/mvc/overview/older-versions-1/controllers-and-routing/asp-net-mvc-routing-overview-cs\",\"https://docs.microsoft.com/en-us/aspnet/core/security/authentication/identity?view=aspnetcore-2.1&tabs=visual-studio\",\"https://stackoverflow.com/questions/10848086/authorize-attribute-in-asp-net-mvc#10848142\"],\"app\":\"steemit/0.1\",\"format\":\"markdown\"}"
    }
  ]
}
2018/11/23 21:57:12
voterraise-me-up
authormarnee
permlinksaturn-with-cas-single-sign-on-sample-application
weight1 (0.01%)
Transaction InfoBlock #27962944/Trx 8ae38cf72f8c9c4b3ac1eda7e20155d21608f8d5
View Raw JSON Data
{
  "trx_id": "8ae38cf72f8c9c4b3ac1eda7e20155d21608f8d5",
  "block": 27962944,
  "trx_in_block": 15,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2018-11-23T21:57:12",
  "op": [
    "vote",
    {
      "voter": "raise-me-up",
      "author": "marnee",
      "permlink": "saturn-with-cas-single-sign-on-sample-application",
      "weight": 1
    }
  ]
}
2018/11/23 21:52:42
parent author
parent permlinkfsharp
authormarnee
permlinksaturn-with-cas-single-sign-on-sample-application
titleSaturn with CAS Single Sign-On Sample Application
body# Saturn with CAS Single Sign-On Sample Application Lessons learned building a web application built on Saturn and using CAS for single sign-on. ## Sample code You can find all of my sample code on my [Github](https://github.com/MarneeDear/saturn-example). ## Why am I doing this? > I decided to try out Saturn to see if we can't start building 100% F# web apps. But first I needed to make CAS work. At the [University of Arizona, College of Medicine - Tucson](https://medicine.arizona.edu/) we build a lot of web apps. As the Applications Architect I have tried to use as much F# as possible, but we usually ended up with a hybrid of F# and C#. This looked like F# for Core and Infrastructure, and C# for the web host on .NET MVC. This worked great but it is not F#, or functional programming, all the way through. But we don't have to do that anymore now that we have F# web frameworks like [Giraffe](https://github.com/giraffe-fsharp/Giraffe) and [Saturn](https://saturnframework.org/). ## A little about Saturn ![Alt text](https://saturnframework.org/assets/img/logo.png) There are a lot of great things to like about Saturn. * Easy to scaffold with the .NET SDK * Built on ASP.NET Core, Giraffe and Kestrel, so it is cross-platform. * A number of [useful sample apps](https://github.com/SaturnFramework/Saturn/tree/master/sample). * Useful extensions and abstractions for things like OAuth and Azure Functions. * Supports .NET Core and .NET Standard. and ... ## Saturn is opinionated! And I like it. Once you get used to the patterns and abstractions, it makes a lot of sense and is easy to make things work. I especially like the use of [computation expressions](https://www.youtube.com/watch?v=bYor0oBgvws). You can find the list of computation expressions used in Saturn in the [API Reference](https://saturnframework.org/docs/api/pipeline/). We have these computation expressions: * Pipeline * Router * Controller * Application We use these, and combinations of them, to build the wider application architecture. ## Getting started Follow the documentation [here](https://saturnframework.org/docs/guides/how-to-start/). > Pro Tip: Make sure you have the [.NET SDK and .NET Runtime](https://www.microsoft.com/net/download) installed. As of 2018-11-18, the project template requires .NET SDK version `2.1.300`. So after creating the project, check `global.json` for the version and either change it to the version you have installed or install the version in `global.json`. You can install multiple versions at one time (older versions can be found at the download link above). If you don't have the right SDK version installed you'll probably see an error like this during the build. ```text A compatible SDK version for global.json version: [2.1.409] from [C:\Users\Marnee\Dropbox\github\saturn-stuff\saturn-onion-template\global.json] was not found Did you mean to run dotnet SDK commands? Please install dotnet SDK from: http://go.microsoft.com/fwlink/?LinkID=798306&clcid=0x409 ``` > Pro Tip: Be careful with dashes in names. By default, `dotnet` will name your project according to the folder in which it is contained, but it converts dashes to underscores, which throws off file paths. You might see an error that looks like this: ```text System.Exception: Start of process dotnet failed. WorkingDir C:\Users\Marnee\Dropbox\github\saturn-stuff\saturn-blog\src\saturn_blog\ does not exist. ``` ## My project (saturn-example) Here is what you get from my [repo on Github](https://github.com/MarneeDear/saturn-example): * [CAS](https://en.wikipedia.org/wiki/Central_Authentication_Service) integration for single-sign-on. This is similar to OAuth and uses [this CAS library](https://www.nuget.org/packages/AspNetCore.Security.CAS) from [Indiana University Foundation](https://github.com/iuCrimson/aspnet.security.cas). * A simple [Onion Architecture](https://www.c-sharpcorner.com/UploadFile/dhananjaycoder/step-by-step-implementing-onion-architecture-in-Asp-Net-mvc/) example. There is a Core library where you put your models, and Infrastructure where you put your business logic and data access. * Test projects using [xUnit](https://xunit.github.io/) and [FsUnit](https://fsprojects.github.io/FsUnit/xUnit.html). ### CAS Single-Sign-On (Authentication) Saturn doesn't have built-in CAS support, but it does have OAuth with GitHub, Google, and custom providers, which is great, but I need CAS, so I had to integrate it myself. This turned out to be pretty easy because I found a compatible [CAS auth library](https://github.com/iuCrimson/aspnet.security.cas) available on Nuget, which meant I could install it with `paket`. Once imported, I could implement it by creating a new module with an`ApplicationBuilder` class with a new `CustomOperation` method. This will make it so I can use it in the [`application` computation expression](https://saturnframework.org/docs/api/application/#application-builder). ```fsharp module CAS open Saturn open Microsoft.Extensions.DependencyInjection open Microsoft.AspNetCore.Builder open Microsoft.AspNetCore.Authentication.Cookies open Microsoft.AspNetCore.Authentication open AspNetCore.Security.CAS type ApplicationBuilder with //Enables CAS authentication //Uses https://github.com/IUCrimson/AspNet.Security.CAS [<CustomOperation("use_cas")>] member __.UseCasAuthentication(state: ApplicationState, casServerUrlBase) = : : : ``` And then we can use it in our application. In Program.fs, the entry point of the app, we do this. ```fsharp let app = application { pipe_through endpointPipe error_handler (fun ex _ -> pipeline { render_html (InternalError.layout ex) }) use_router Router.appRouter url "http://saturn.local:8085/" memory_cache use_static "static" use_gzip use_config (fun _ -> {connectionString = "DataSource=database.sqlite"} ) use_iis use_cas "https://webauth.arizona.edu/webauth" } ``` You can see the full source code on my GitHub repo: * The entry point [Program.fs](https://github.com/MarneeDear/saturn-example/blob/master/src/Template.Saturn.WebHost/Program.fs) * The CAS implementation [Cas.fs](https://github.com/MarneeDear/saturn-example/blob/master/src/Template.Saturn.WebHost/Authentication/Cas.fs) ### The CAS implementation I followed the pattern used in the Saturn OAuth implementations. `use_cas` takes two arguments: * `casServerUrlBase` -- your CAS server's authentication URL. In my case I am using my University's CAS server known as `webauth`. * `state` -- `ApplicationState` defined in Saturn [here](https://github.com/SaturnFramework/Saturn/blob/master/src/Saturn/Application.fs). In the `application` computation expression, the state is automatically passed to the method. What we want to end up with in `UseCasAuthentication` is a new state with the CAS authentication configuration added to the old state. I defined a middleware function in which I enabled [`UseAuthentication`](https://docs.microsoft.com/tr-tr/dotnet/api/microsoft.aspnetcore.builder.authappbuilderextensions.useauthentication?view=aspnetcore-2.0#Microsoft_AspNetCore_Builder_AuthAppBuilderExtensions_UseAuthentication_Microsoft_AspNetCore_Builder_IApplicationBuilder_). ```fsharp let middleware (app : IApplicationBuilder) = app.UseAuthentication() ``` I defined a service function which configures the CAS authentication. I followed the [CAS library](https://github.com/iuCrimson/aspnet.security.cas) guide. ```fsharp let service (s : IServiceCollection) = let c = s.AddAuthentication(fun cfg -> cfg.DefaultScheme <- CookieAuthenticationDefaults.AuthenticationScheme cfg.DefaultChallengeScheme <- "CAS" ) addCookie state c c.AddCAS(fun o -> o.CasServerUrlBase <- casServerUrlBase o.SignInScheme <- CookieAuthenticationDefaults.AuthenticationScheme ) |> ignore s ``` Here I configured the default scheme and gave the Challenge Scheme a name (`CAS`). Later, when I want to do a login, I can have the Giraffe Auth challenge use `CAS` like this: ```fsharp (Giraffe.Auth.challenge "CAS") ``` I also configure the `CAS` server URL and the Authentication Scheme. Finally I return a new state with all of the configurations added. Like this: ```fsharp { state with ServicesConfig = service::state.ServicesConfig AppConfigs = middleware::state.AppConfigs CookiesAlreadyAdded = true } ``` ## Make the login work Ok, great, we have added CAS authentication, but how do I login? I need a login button and to add some security that restricts access to resources. We do this with the [`router`](https://saturnframework.org/docs/api/scope/) and [`pipeline`](https://saturnframework.org/docs/api/pipeline/) computation expressions. > Note: Router in the Saturn user guide is a outdated. It still references `scope`, which was deprecated in favor of `router`. I did a pull request to update this in the source files, but as of 11/23/2018 the published guide has not been updated. Router defines paths and routes through your application. It has a lot in common with [.NET MVC routing](https://docs.microsoft.com/en-us/aspnet/mvc/overview/older-versions-1/controllers-and-routing/asp-net-mvc-routing-overview-cs). ### Router.fs First I need a top-level-router which will handle all requests to the app. This might route requests to a controller, a pipeline, or another router. In my apps I need a public side, usually the login page, and a private side that only authenticated users can access. To handle this I created both a top-level router, `browserRouter`, and a `loggedInView` router. *logged-in view router* ```fsharp let loggedInView = router { pipe_through login pipe_through protectFromForgery forward "/books" Books.Controller.resource forward "/dashboard" (fun next ctx -> htmlView (Dashboard.layout ctx) next ctx) } ``` Requests that pass through the `loggedInView` router are checked for authentication status and sent to webauth if not. `pipe_through login` makes this happen like this: ```fsharp let login = pipeline { requires_authentication (fun next ctx -> htmlView (Login.layout ctx) next ctx) } ``` The important part is `requires_authentication`. Is a `CustomOperation` `PipelineBuilder` which checks for authentication. In my case, here, if not authenticated, the user will be sent to the login page. *top-level router* ```fsharp let browserRouter = router { not_found_handler (htmlView NotFound.layout) //Use the default 404 webpage pipe_through browser //Use the default browser pipeline forward "" defaultView //Use the default view get "/books" loggedInView get "/login" (fun next ctx -> htmlView (Login.layout ctx) next ctx) get "/logout" (signOut "Cookies" >=> (fun next ctx -> htmlView (Logout.layout ctx) next ctx)) get "/dashboard" loggedInView get "/webauth" (fun next ctx -> (isAuthenticated ctx) next ctx) } ``` * `/login` gets the login page but does not send the user to `webauth` * `/logout` sign-out the user (clears auth cookies) and get the logout page * `/webauth` check if the user has been authenticated and send the user to webauth if not authenticated. * `/books` and `/dashboard` are private pages so they go through the `loggedInView` router ## Logged-in view layout template vs. public view layout template I had a problem with the App level layout. This layout had the login button and the menu and each was toggled based on the app context passed to it. Using the context I would check for authentication status and toggle the login and menu. The problem was that the menu would not get replaced with the login button after logout. In order to deal with this I created two app level layouts, instead. * App.fs is the public view layout * AppAuth.fs is the private view layout In the public views I plug into the public view layout like this: ```fsharp let layout ctx = App.layout (login ctx) ctx ``` In the private views I plug into the logged-in view layout like this: ```fsharp let layout ctx = AuthApp.layout (dashboard ctx) ctx ``` ## What's next? ![Moar?](http://www.vitamin-ha.com/wp-content/uploads/2013/05/Now-What-Meme.jpg) I need to figure out these things: * What am I going to use for authorization? Do I want to build a sample-app that also uses [ASP.NET Core Identity](https://docs.microsoft.com/en-us/aspnet/core/security/authentication/identity?view=aspnetcore-2.1&tabs=visual-studio)? Maybe I'll roll my own with EDS/Grouper for claims and membership, and a simple database to assign roles and privileges, like I have in previous projects. * Figure out how to restrict access to controllers and views like I do with the [`Authorize` attribute](https://stackoverflow.com/questions/10848086/authorize-attribute-in-asp-net-mvc#10848142) in .NET MVC or .NET Web API 2.0. I suspect I will have to fig into Giraffe for this. ## Final thoughts Working with Saturn was a bit painful at first as I was trying to learn the abstractions and `opinions`, but eventually it all just made a lot of sense and it is pretty easy to work with. Try it out! ![Aha!](https://i.gr-assets.com/images/S/compressed.photo.goodreads.com/hostedimages/1381270478i/4622285.jpg)
json metadata{"tags":["fsharp","functional","programming","aspnet","cas"],"image":["https://saturnframework.org/assets/img/logo.png","http://www.vitamin-ha.com/wp-content/uploads/2013/05/Now-What-Meme.jpg","https://i.gr-assets.com/images/S/compressed.photo.goodreads.com/hostedimages/1381270478i/4622285.jpg"],"links":["https://github.com/MarneeDear/saturn-example","https://medicine.arizona.edu/","https://github.com/giraffe-fsharp/Giraffe","https://saturnframework.org/","https://github.com/SaturnFramework/Saturn/tree/master/sample","https://www.youtube.com/watch?v=bYor0oBgvws","https://saturnframework.org/docs/api/pipeline/","https://saturnframework.org/docs/guides/how-to-start/","https://www.microsoft.com/net/download","https://en.wikipedia.org/wiki/Central_Authentication_Service","https://www.nuget.org/packages/AspNetCore.Security.CAS","https://github.com/iuCrimson/aspnet.security.cas","https://www.c-sharpcorner.com/UploadFile/dhananjaycoder/step-by-step-implementing-onion-architecture-in-Asp-Net-mvc/","https://xunit.github.io/","https://fsprojects.github.io/FsUnit/xUnit.html","https://saturnframework.org/docs/api/application/#application-builder","https://github.com/MarneeDear/saturn-example/blob/master/src/Template.Saturn.WebHost/Program.fs","https://github.com/MarneeDear/saturn-example/blob/master/src/Template.Saturn.WebHost/Authentication/Cas.fs","https://github.com/SaturnFramework/Saturn/blob/master/src/Saturn/Application.fs","https://docs.microsoft.com/tr-tr/dotnet/api/microsoft.aspnetcore.builder.authappbuilderextensions.useauthentication?view=aspnetcore-2.0#Microsoft_AspNetCore_Builder_AuthAppBuilderExtensions_UseAuthentication_Microsoft_AspNetCore_Builder_IApplicationBuilder_","https://saturnframework.org/docs/api/scope/","https://docs.microsoft.com/en-us/aspnet/mvc/overview/older-versions-1/controllers-and-routing/asp-net-mvc-routing-overview-cs","https://docs.microsoft.com/en-us/aspnet/core/security/authentication/identity?view=aspnetcore-2.1&tabs=visual-studio","https://stackoverflow.com/questions/10848086/authorize-attribute-in-asp-net-mvc#10848142"],"app":"steemit/0.1","format":"markdown"}
Transaction InfoBlock #27962854/Trx b82490164fcd3a8184d90226effe06b20a5af8fa
View Raw JSON Data
{
  "trx_id": "b82490164fcd3a8184d90226effe06b20a5af8fa",
  "block": 27962854,
  "trx_in_block": 11,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2018-11-23T21:52:42",
  "op": [
    "comment",
    {
      "parent_author": "",
      "parent_permlink": "fsharp",
      "author": "marnee",
      "permlink": "saturn-with-cas-single-sign-on-sample-application",
      "title": "Saturn with CAS Single Sign-On Sample Application",
      "body": "# Saturn with CAS Single Sign-On Sample Application\n\nLessons learned building a web application built on Saturn and using CAS for single sign-on.\n\n## Sample code\n\nYou can find all of my sample code on my [Github](https://github.com/MarneeDear/saturn-example).\n\n## Why am I doing this?\n\n> I decided to try out Saturn to see if we can't start building 100% F# web apps. But first I needed to make CAS work.\n\nAt the [University of Arizona, College of Medicine - Tucson](https://medicine.arizona.edu/) we build a lot of web apps. As the Applications Architect I have tried to use as much F# as possible, but we usually ended up with a hybrid of F# and C#. This looked like F# for Core and Infrastructure, and C# for the web host on .NET MVC. This worked great but it is not F#, or functional programming, all the way through. But we don't have to do that anymore now that we have F# web frameworks like [Giraffe](https://github.com/giraffe-fsharp/Giraffe) and [Saturn](https://saturnframework.org/).\n\n## A little about Saturn\n\n![Alt text](https://saturnframework.org/assets/img/logo.png)\n\nThere are a lot of great things to like about Saturn.\n\n* Easy to scaffold with the .NET SDK\n* Built on ASP.NET Core, Giraffe and Kestrel, so it is cross-platform.\n* A number of [useful sample apps](https://github.com/SaturnFramework/Saturn/tree/master/sample).\n* Useful extensions and abstractions for things like OAuth and Azure Functions.\n* Supports .NET Core and .NET Standard.\n\nand ...\n\n## Saturn is opinionated! \n\nAnd I like it. Once you get used to the patterns and abstractions, it makes a lot of sense and is easy to make things work.\n\nI especially like the use of [computation expressions](https://www.youtube.com/watch?v=bYor0oBgvws).\n\nYou can find the list of computation expressions used in Saturn in the [API Reference](https://saturnframework.org/docs/api/pipeline/).\n\nWe have these computation expressions:\n\n* Pipeline\n* Router\n* Controller\n* Application\n\nWe use these, and combinations of them, to build the wider application architecture.\n\n## Getting started\n\nFollow the documentation [here](https://saturnframework.org/docs/guides/how-to-start/).\n\n> Pro Tip: Make sure you have the [.NET SDK and .NET Runtime](https://www.microsoft.com/net/download) installed.\n\nAs of 2018-11-18, the project template requires .NET SDK version `2.1.300`. So after creating the project, check `global.json` for the version and either change it to the version you have installed or install the version in `global.json`. You can install multiple versions at one time (older versions can be found at the download link above).\n\nIf you don't have the right SDK version installed you'll probably see an error like this during the build.\n\n```text\nA compatible SDK version for global.json version: [2.1.409] from [C:\\Users\\Marnee\\Dropbox\\github\\saturn-stuff\\saturn-onion-template\\global.json] was not found\nDid you mean to run dotnet SDK commands? Please install dotnet SDK from:\n  http://go.microsoft.com/fwlink/?LinkID=798306&clcid=0x409\n```\n\n> Pro Tip: Be careful with dashes in names. By default, `dotnet` will name your project according to the folder in which it is contained, but it converts dashes to underscores, which throws off file paths. You might see an error that looks like this:\n\n```text\nSystem.Exception: Start of process dotnet failed. WorkingDir C:\\Users\\Marnee\\Dropbox\\github\\saturn-stuff\\saturn-blog\\src\\saturn_blog\\ does not exist.\n```\n\n## My project (saturn-example)\n\nHere is what you get from my [repo on Github](https://github.com/MarneeDear/saturn-example):\n\n* [CAS](https://en.wikipedia.org/wiki/Central_Authentication_Service) integration for single-sign-on. This is similar to OAuth and uses [this CAS library](https://www.nuget.org/packages/AspNetCore.Security.CAS) from [Indiana University Foundation](https://github.com/iuCrimson/aspnet.security.cas).\n* A simple [Onion Architecture](https://www.c-sharpcorner.com/UploadFile/dhananjaycoder/step-by-step-implementing-onion-architecture-in-Asp-Net-mvc/) example. There is a Core library where you put your models, and Infrastructure where you put your business logic and data access.\n* Test projects using [xUnit](https://xunit.github.io/) and [FsUnit](https://fsprojects.github.io/FsUnit/xUnit.html).\n\n### CAS Single-Sign-On (Authentication)\n\nSaturn doesn't have built-in CAS support, but it does have OAuth with GitHub, Google, and custom providers, which is great, but I need CAS, so I had to integrate it myself. This turned out to be pretty easy because I found a compatible [CAS auth library](https://github.com/iuCrimson/aspnet.security.cas) available on Nuget, which meant I could install it with `paket`.\n\nOnce imported, I could implement it by creating a new module with an`ApplicationBuilder` class with a new `CustomOperation` method. This will make it so I can use it in the [`application` computation expression](https://saturnframework.org/docs/api/application/#application-builder).\n\n```fsharp\nmodule CAS\n\nopen Saturn\nopen Microsoft.Extensions.DependencyInjection\nopen Microsoft.AspNetCore.Builder\nopen Microsoft.AspNetCore.Authentication.Cookies\nopen Microsoft.AspNetCore.Authentication\nopen AspNetCore.Security.CAS\n\ntype ApplicationBuilder with\n    //Enables CAS authentication\n    //Uses https://github.com/IUCrimson/AspNet.Security.CAS\n    [<CustomOperation(\"use_cas\")>]\n    member __.UseCasAuthentication(state: ApplicationState, casServerUrlBase) =\n:\n:\n:\n```\n\nAnd then we can use it in our application. In Program.fs, the entry point of the app, we do this.\n\n```fsharp\nlet app = application {\n    pipe_through endpointPipe\n    error_handler (fun ex _ -> pipeline { render_html (InternalError.layout ex) })\n    use_router Router.appRouter\n    url \"http://saturn.local:8085/\"\n    memory_cache\n    use_static \"static\"\n    use_gzip\n    use_config (fun _ -> {connectionString = \"DataSource=database.sqlite\"} ) \n    use_iis\n    use_cas \"https://webauth.arizona.edu/webauth\"\n}\n```\n\nYou can see the full source code on my GitHub repo:\n\n* The entry point [Program.fs](https://github.com/MarneeDear/saturn-example/blob/master/src/Template.Saturn.WebHost/Program.fs)\n* The CAS implementation [Cas.fs](https://github.com/MarneeDear/saturn-example/blob/master/src/Template.Saturn.WebHost/Authentication/Cas.fs)\n\n### The CAS implementation\n\nI followed the pattern used in the Saturn OAuth implementations.\n\n`use_cas` takes two arguments:\n\n* `casServerUrlBase` -- your CAS server's authentication URL. In my case I am using my University's CAS server known as `webauth`.\n* `state` -- `ApplicationState` defined in Saturn [here](https://github.com/SaturnFramework/Saturn/blob/master/src/Saturn/Application.fs). In the `application` computation expression, the state is automatically passed to the method.\n\nWhat we want to end up with in `UseCasAuthentication` is a new state with the CAS authentication configuration added to the old state.\n\nI defined a middleware function in which I enabled [`UseAuthentication`](https://docs.microsoft.com/tr-tr/dotnet/api/microsoft.aspnetcore.builder.authappbuilderextensions.useauthentication?view=aspnetcore-2.0#Microsoft_AspNetCore_Builder_AuthAppBuilderExtensions_UseAuthentication_Microsoft_AspNetCore_Builder_IApplicationBuilder_).\n\n```fsharp\n  let middleware (app : IApplicationBuilder) =\n    app.UseAuthentication()\n```\n\nI defined a service function which configures the CAS authentication. I followed the [CAS library](https://github.com/iuCrimson/aspnet.security.cas) guide.\n\n```fsharp\n  let service (s : IServiceCollection) =\n    let c = s.AddAuthentication(fun cfg ->\n      cfg.DefaultScheme <- CookieAuthenticationDefaults.AuthenticationScheme\n      cfg.DefaultChallengeScheme <- \"CAS\"\n      )\n    addCookie state c\n    c.AddCAS(fun o -> \n        o.CasServerUrlBase <- casServerUrlBase\n        o.SignInScheme <- CookieAuthenticationDefaults.AuthenticationScheme\n        )\n    |> ignore\n    s\n```\n\nHere I configured the default scheme and gave the Challenge Scheme a name (`CAS`). Later, when I want to do a login, I can have the Giraffe Auth challenge use `CAS` like this:\n\n```fsharp\n  (Giraffe.Auth.challenge \"CAS\")\n```\n\nI also configure the `CAS` server URL and the Authentication Scheme.\n\nFinally I return a new state with all of the configurations added. Like this:\n\n```fsharp\n  { state with\n      ServicesConfig = service::state.ServicesConfig\n      AppConfigs = middleware::state.AppConfigs\n      CookiesAlreadyAdded = true\n  }\n```\n\n## Make the login work\n\nOk, great, we have added CAS authentication, but how do I login? I need a login button and to add some security that restricts access to resources. We do this with the [`router`](https://saturnframework.org/docs/api/scope/) and [`pipeline`](https://saturnframework.org/docs/api/pipeline/) computation expressions.\n\n> Note: Router in the Saturn user guide is a outdated. It still references `scope`, which was deprecated in favor of `router`. I did a pull request to update this in the source files, but as of 11/23/2018 the published guide has not been updated.\n\nRouter defines paths and routes through your application. It has a lot in common with [.NET MVC routing](https://docs.microsoft.com/en-us/aspnet/mvc/overview/older-versions-1/controllers-and-routing/asp-net-mvc-routing-overview-cs).\n\n### Router.fs\n\nFirst I need a top-level-router which will handle all requests to the app. This might route requests to a controller, a pipeline, or another router. In my apps I need a public side, usually the login page, and a private side that only authenticated users can access. To handle this I created both a top-level router, `browserRouter`, and a `loggedInView` router.\n\n*logged-in view router*\n\n```fsharp\nlet loggedInView = router {\n    pipe_through login\n    pipe_through protectFromForgery\n    forward \"/books\" Books.Controller.resource \n    forward \"/dashboard\" (fun next ctx -> htmlView (Dashboard.layout ctx) next ctx)\n}\n```\n\nRequests that pass through the `loggedInView` router are checked for authentication status and sent to webauth if not. `pipe_through login` makes this happen like this:\n\n```fsharp\nlet login = pipeline {\n    requires_authentication (fun next ctx -> htmlView (Login.layout ctx) next ctx)\n}\n```\n\nThe important part is `requires_authentication`. Is a `CustomOperation` `PipelineBuilder` which checks for authentication. In my case, here, if not authenticated, the user will be sent to the login page.\n\n*top-level router*\n\n```fsharp\nlet browserRouter = router {\n    not_found_handler (htmlView NotFound.layout) //Use the default 404 webpage\n    pipe_through browser //Use the default browser pipeline\n    forward \"\" defaultView //Use the default view\n    get \"/books\" loggedInView\n    get \"/login\" (fun next ctx -> htmlView (Login.layout ctx) next ctx)\n    get \"/logout\" (signOut \"Cookies\" >=> (fun next ctx -> htmlView (Logout.layout ctx) next ctx)) \n    get \"/dashboard\" loggedInView \n    get \"/webauth\" (fun next ctx -> (isAuthenticated ctx) next ctx)\n}\n```\n\n* `/login` gets the login page but does not send the user to `webauth`\n* `/logout` sign-out the user (clears auth cookies) and get the logout page\n* `/webauth` check if the user has been authenticated and send the user to webauth if not authenticated.\n* `/books` and `/dashboard` are private pages so they go through the `loggedInView` router\n\n## Logged-in view layout template vs. public view layout template\n\nI had a problem with the App level layout. This layout had the login button and the menu and each was toggled based on the app context passed to it. Using the context I would check for authentication status and toggle the login and menu. The problem was that the menu would not get replaced with the login button after logout. In order to deal with this I created two app level layouts, instead.\n\n* App.fs is the public view layout\n* AppAuth.fs is the private view layout\n\nIn the public views I plug into the public view layout like this:\n\n```fsharp\nlet layout ctx =\n    App.layout (login ctx) ctx\n```\n\nIn the private views I plug into the logged-in view layout like this:\n\n```fsharp\nlet layout ctx =\n    AuthApp.layout (dashboard ctx) ctx\n```\n\n## What's next?\n\n![Moar?](http://www.vitamin-ha.com/wp-content/uploads/2013/05/Now-What-Meme.jpg)\n\nI need to figure out these things:\n\n* What am I going to use for authorization? Do I want to build a sample-app that also uses [ASP.NET Core Identity](https://docs.microsoft.com/en-us/aspnet/core/security/authentication/identity?view=aspnetcore-2.1&tabs=visual-studio)? Maybe I'll roll my own with EDS/Grouper for claims and membership, and a simple database to assign roles and privileges, like I have in previous projects.\n* Figure out how to restrict access to controllers and views like I do with the [`Authorize` attribute](https://stackoverflow.com/questions/10848086/authorize-attribute-in-asp-net-mvc#10848142) in .NET MVC or .NET Web API 2.0. I suspect I will have to fig into Giraffe for this.\n\n## Final thoughts\n\nWorking with Saturn was a bit painful at first as I was trying to learn the abstractions and `opinions`, but eventually it all just made a lot of sense and it is pretty easy to work with. Try it out!\n\n![Aha!](https://i.gr-assets.com/images/S/compressed.photo.goodreads.com/hostedimages/1381270478i/4622285.jpg)",
      "json_metadata": "{\"tags\":[\"fsharp\",\"functional\",\"programming\",\"aspnet\",\"cas\"],\"image\":[\"https://saturnframework.org/assets/img/logo.png\",\"http://www.vitamin-ha.com/wp-content/uploads/2013/05/Now-What-Meme.jpg\",\"https://i.gr-assets.com/images/S/compressed.photo.goodreads.com/hostedimages/1381270478i/4622285.jpg\"],\"links\":[\"https://github.com/MarneeDear/saturn-example\",\"https://medicine.arizona.edu/\",\"https://github.com/giraffe-fsharp/Giraffe\",\"https://saturnframework.org/\",\"https://github.com/SaturnFramework/Saturn/tree/master/sample\",\"https://www.youtube.com/watch?v=bYor0oBgvws\",\"https://saturnframework.org/docs/api/pipeline/\",\"https://saturnframework.org/docs/guides/how-to-start/\",\"https://www.microsoft.com/net/download\",\"https://en.wikipedia.org/wiki/Central_Authentication_Service\",\"https://www.nuget.org/packages/AspNetCore.Security.CAS\",\"https://github.com/iuCrimson/aspnet.security.cas\",\"https://www.c-sharpcorner.com/UploadFile/dhananjaycoder/step-by-step-implementing-onion-architecture-in-Asp-Net-mvc/\",\"https://xunit.github.io/\",\"https://fsprojects.github.io/FsUnit/xUnit.html\",\"https://saturnframework.org/docs/api/application/#application-builder\",\"https://github.com/MarneeDear/saturn-example/blob/master/src/Template.Saturn.WebHost/Program.fs\",\"https://github.com/MarneeDear/saturn-example/blob/master/src/Template.Saturn.WebHost/Authentication/Cas.fs\",\"https://github.com/SaturnFramework/Saturn/blob/master/src/Saturn/Application.fs\",\"https://docs.microsoft.com/tr-tr/dotnet/api/microsoft.aspnetcore.builder.authappbuilderextensions.useauthentication?view=aspnetcore-2.0#Microsoft_AspNetCore_Builder_AuthAppBuilderExtensions_UseAuthentication_Microsoft_AspNetCore_Builder_IApplicationBuilder_\",\"https://saturnframework.org/docs/api/scope/\",\"https://docs.microsoft.com/en-us/aspnet/mvc/overview/older-versions-1/controllers-and-routing/asp-net-mvc-routing-overview-cs\",\"https://docs.microsoft.com/en-us/aspnet/core/security/authentication/identity?view=aspnetcore-2.1&tabs=visual-studio\",\"https://stackoverflow.com/questions/10848086/authorize-attribute-in-asp-net-mvc#10848142\"],\"app\":\"steemit/0.1\",\"format\":\"markdown\"}"
    }
  ]
}
steemdelegated 17.953 SP to @marnee
2018/10/23 04:53:18
delegatorsteem
delegateemarnee
vesting shares29234.416837 VESTS
Transaction InfoBlock #27050284/Trx 9ce0de2e55e30d8238bada8b89426ddd899ab603
View Raw JSON Data
{
  "trx_id": "9ce0de2e55e30d8238bada8b89426ddd899ab603",
  "block": 27050284,
  "trx_in_block": 17,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2018-10-23T04:53:18",
  "op": [
    "delegate_vesting_shares",
    {
      "delegator": "steem",
      "delegatee": "marnee",
      "vesting_shares": "29234.416837 VESTS"
    }
  ]
}
2018/10/23 02:31:45
votermarnee
authorkafkanarchy84
permlinkthe-attempted-terrorism-against-myself-and-my-family-from-the-adamkokesh-2020-campaign-would-have-entailed-the-following
weight10000 (100.00%)
Transaction InfoBlock #27047454/Trx 01df24efbe8e3a4392e0497f32a18b2ffbb273ae
View Raw JSON Data
{
  "trx_id": "01df24efbe8e3a4392e0497f32a18b2ffbb273ae",
  "block": 27047454,
  "trx_in_block": 29,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2018-10-23T02:31:45",
  "op": [
    "vote",
    {
      "voter": "marnee",
      "author": "kafkanarchy84",
      "permlink": "the-attempted-terrorism-against-myself-and-my-family-from-the-adamkokesh-2020-campaign-would-have-entailed-the-following",
      "weight": 10000
    }
  ]
}
2018/10/23 02:29:36
required auths[]
required posting auths["marnee"]
idfollow
json["follow",{"follower":"marnee","following":"kafkanarchy84","what":["blog"]}]
Transaction InfoBlock #27047411/Trx 227dca09a56311b033068486e572c665b08e3716
View Raw JSON Data
{
  "trx_id": "227dca09a56311b033068486e572c665b08e3716",
  "block": 27047411,
  "trx_in_block": 0,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2018-10-23T02:29:36",
  "op": [
    "custom_json",
    {
      "required_auths": [],
      "required_posting_auths": [
        "marnee"
      ],
      "id": "follow",
      "json": "[\"follow\",{\"follower\":\"marnee\",\"following\":\"kafkanarchy84\",\"what\":[\"blog\"]}]"
    }
  ]
}
2018/10/11 02:16:06
voteralkasai
authormarnee
permlinkplugging-in-elm-and-suave-with-websockets-on-net-core-2-0
weight10000 (100.00%)
Transaction InfoBlock #26701808/Trx b4da9c20735f57b817ffd0005b0a974a421d3160
View Raw JSON Data
{
  "trx_id": "b4da9c20735f57b817ffd0005b0a974a421d3160",
  "block": 26701808,
  "trx_in_block": 23,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2018-10-11T02:16:06",
  "op": [
    "vote",
    {
      "voter": "alkasai",
      "author": "marnee",
      "permlink": "plugging-in-elm-and-suave-with-websockets-on-net-core-2-0",
      "weight": 10000
    }
  ]
}
2018/10/11 02:07:33
voteralkasai
authormarnee
permlinksuave-websocket-server-with-a-continuous-feed
weight10000 (100.00%)
Transaction InfoBlock #26701637/Trx c8d4337fdb933281c56fcf2c308597f5a3916e9d
View Raw JSON Data
{
  "trx_id": "c8d4337fdb933281c56fcf2c308597f5a3916e9d",
  "block": 26701637,
  "trx_in_block": 16,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2018-10-11T02:07:33",
  "op": [
    "vote",
    {
      "voter": "alkasai",
      "author": "marnee",
      "permlink": "suave-websocket-server-with-a-continuous-feed",
      "weight": 10000
    }
  ]
}
steemdelegated 5.613 SP to @marnee
2018/05/20 19:12:12
delegatorsteem
delegateemarnee
vesting shares9140.213739 VESTS
Transaction InfoBlock #22603724/Trx f0edd208db01c8e769e56d4c165776aff45a5deb
View Raw JSON Data
{
  "trx_id": "f0edd208db01c8e769e56d4c165776aff45a5deb",
  "block": 22603724,
  "trx_in_block": 18,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2018-05-20T19:12:12",
  "op": [
    "delegate_vesting_shares",
    {
      "delegator": "steem",
      "delegatee": "marnee",
      "vesting_shares": "9140.213739 VESTS"
    }
  ]
}
steemdelegated 18.193 SP to @marnee
2018/02/22 12:25:12
delegatorsteem
delegateemarnee
vesting shares29624.234587 VESTS
Transaction InfoBlock #20092532/Trx 8d2494ebaaadc44e592dffa003e03a541db53036
View Raw JSON Data
{
  "trx_id": "8d2494ebaaadc44e592dffa003e03a541db53036",
  "block": 20092532,
  "trx_in_block": 20,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2018-02-22T12:25:12",
  "op": [
    "delegate_vesting_shares",
    {
      "delegator": "steem",
      "delegatee": "marnee",
      "vesting_shares": "29624.234587 VESTS"
    }
  ]
}
2018/02/18 18:28:24
required auths[]
required posting auths["marnee"]
idfollow
json["follow",{"follower":"marnee","following":"lisperati","what":["blog"]}]
Transaction InfoBlock #19984631/Trx 2b48747bd4fc01902df23a87265513ed5a44c25b
View Raw JSON Data
{
  "trx_id": "2b48747bd4fc01902df23a87265513ed5a44c25b",
  "block": 19984631,
  "trx_in_block": 34,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2018-02-18T18:28:24",
  "op": [
    "custom_json",
    {
      "required_auths": [],
      "required_posting_auths": [
        "marnee"
      ],
      "id": "follow",
      "json": "[\"follow\",{\"follower\":\"marnee\",\"following\":\"lisperati\",\"what\":[\"blog\"]}]"
    }
  ]
}
2018/02/10 06:35:18
parent authormarnee
parent permlinkjoining-the-southern-arizona-ham-mesh-network
authorpablop
permlinkre-marnee-joining-the-southern-arizona-ham-mesh-network-20180210t063519240z
title
bodyI’m a ham in Phoenix. Nice article! Good to find a ham on Steemit. 73, paul
json metadata{"tags":["amatuer"],"app":"steemit/0.1"}
Transaction InfoBlock #19740157/Trx 077ebd17d071bc4dff24c10a87d8e35e1850a861
View Raw JSON Data
{
  "trx_id": "077ebd17d071bc4dff24c10a87d8e35e1850a861",
  "block": 19740157,
  "trx_in_block": 49,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2018-02-10T06:35:18",
  "op": [
    "comment",
    {
      "parent_author": "marnee",
      "parent_permlink": "joining-the-southern-arizona-ham-mesh-network",
      "author": "pablop",
      "permlink": "re-marnee-joining-the-southern-arizona-ham-mesh-network-20180210t063519240z",
      "title": "",
      "body": "I’m a ham in Phoenix. Nice article!  Good to find a ham on Steemit. 73, paul",
      "json_metadata": "{\"tags\":[\"amatuer\"],\"app\":\"steemit/0.1\"}"
    }
  ]
}
2018/01/20 23:43:45
required auths[]
required posting auths["marnee"]
idfollow
json["follow",{"follower":"marnee","following":"themarkymark","what":["blog"]}]
Transaction InfoBlock #19156643/Trx f769c26168a064baa1f5396e693a58371ca85a0a
View Raw JSON Data
{
  "trx_id": "f769c26168a064baa1f5396e693a58371ca85a0a",
  "block": 19156643,
  "trx_in_block": 113,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2018-01-20T23:43:45",
  "op": [
    "custom_json",
    {
      "required_auths": [],
      "required_posting_auths": [
        "marnee"
      ],
      "id": "follow",
      "json": "[\"follow\",{\"follower\":\"marnee\",\"following\":\"themarkymark\",\"what\":[\"blog\"]}]"
    }
  ]
}
2018/01/16 14:55:51
parent authordimitar9
parent permlinkre-marnee-joining-the-southern-arizona-ham-mesh-network-20180115t235546485z
authormarnee
permlinkre-dimitar9-re-marnee-joining-the-southern-arizona-ham-mesh-network-20180116t145551493z
title
bodyI will have to test it. I haven't been able to get it on the roof yet, but the tallest node in the city is about 8 miles away. I think I might be able to link up with it.
json metadata{"tags":["amatuer"],"app":"steemit/0.1"}
Transaction InfoBlock #19030943/Trx 7eeab1bfdc2fac1a9b4fddb608a25e4119bc90a4
View Raw JSON Data
{
  "trx_id": "7eeab1bfdc2fac1a9b4fddb608a25e4119bc90a4",
  "block": 19030943,
  "trx_in_block": 19,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2018-01-16T14:55:51",
  "op": [
    "comment",
    {
      "parent_author": "dimitar9",
      "parent_permlink": "re-marnee-joining-the-southern-arizona-ham-mesh-network-20180115t235546485z",
      "author": "marnee",
      "permlink": "re-dimitar9-re-marnee-joining-the-southern-arizona-ham-mesh-network-20180116t145551493z",
      "title": "",
      "body": "I will have to test it. I haven't been able to get it on the roof yet, but the tallest node in the city is about 8 miles away. I think I might be able to link up with it.",
      "json_metadata": "{\"tags\":[\"amatuer\"],\"app\":\"steemit/0.1\"}"
    }
  ]
}
2018/01/16 01:43:51
required auths[]
required posting auths["marnee"]
idfollow
json["follow",{"follower":"marnee","following":"dimitar9","what":["blog"]}]
Transaction InfoBlock #19015105/Trx e2764a47dee7902d356a56065db17bde9a0e901f
View Raw JSON Data
{
  "trx_id": "e2764a47dee7902d356a56065db17bde9a0e901f",
  "block": 19015105,
  "trx_in_block": 23,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2018-01-16T01:43:51",
  "op": [
    "custom_json",
    {
      "required_auths": [],
      "required_posting_auths": [
        "marnee"
      ],
      "id": "follow",
      "json": "[\"follow\",{\"follower\":\"marnee\",\"following\":\"dimitar9\",\"what\":[\"blog\"]}]"
    }
  ]
}
marneeupvoted (100.00%) @dimitar9 / daily-gun-show
2018/01/16 01:43:36
votermarnee
authordimitar9
permlinkdaily-gun-show
weight10000 (100.00%)
Transaction InfoBlock #19015100/Trx 9f9dec20500248ad70c9e1e6a3fa92074c3ed74f
View Raw JSON Data
{
  "trx_id": "9f9dec20500248ad70c9e1e6a3fa92074c3ed74f",
  "block": 19015100,
  "trx_in_block": 61,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2018-01-16T01:43:36",
  "op": [
    "vote",
    {
      "voter": "marnee",
      "author": "dimitar9",
      "permlink": "daily-gun-show",
      "weight": 10000
    }
  ]
}
2018/01/15 23:55:48
voterdimitar9
authormarnee
permlinkjoining-the-southern-arizona-ham-mesh-network
weight10000 (100.00%)
Transaction InfoBlock #19012946/Trx 695e0b6486fa498dc546eb3e749278b6b952bb5a
View Raw JSON Data
{
  "trx_id": "695e0b6486fa498dc546eb3e749278b6b952bb5a",
  "block": 19012946,
  "trx_in_block": 20,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2018-01-15T23:55:48",
  "op": [
    "vote",
    {
      "voter": "dimitar9",
      "author": "marnee",
      "permlink": "joining-the-southern-arizona-ham-mesh-network",
      "weight": 10000
    }
  ]
}
2018/01/15 23:55:48
parent authormarnee
parent permlinkjoining-the-southern-arizona-ham-mesh-network
authordimitar9
permlinkre-marnee-joining-the-southern-arizona-ham-mesh-network-20180115t235546485z
title
bodyinteresting. hong long distance can it reach?
json metadata{"tags":["amatuer"],"app":"steemit/0.1"}
Transaction InfoBlock #19012946/Trx c6a50c1348f8a3b11b8bfac559731dc69dfda10d
View Raw JSON Data
{
  "trx_id": "c6a50c1348f8a3b11b8bfac559731dc69dfda10d",
  "block": 19012946,
  "trx_in_block": 13,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2018-01-15T23:55:48",
  "op": [
    "comment",
    {
      "parent_author": "marnee",
      "parent_permlink": "joining-the-southern-arizona-ham-mesh-network",
      "author": "dimitar9",
      "permlink": "re-marnee-joining-the-southern-arizona-ham-mesh-network-20180115t235546485z",
      "title": "",
      "body": "interesting.  hong long distance can it reach?",
      "json_metadata": "{\"tags\":[\"amatuer\"],\"app\":\"steemit/0.1\"}"
    }
  ]
}
2018/01/15 23:50:15
votermarnee
authordanielkdewar
permlinkthe-perfect-description-of-blockchain-technology-why-it-s-becoming-more-simple
weight10000 (100.00%)
Transaction InfoBlock #19012835/Trx a24f514e24fac543ee980b761066d25f7845322a
View Raw JSON Data
{
  "trx_id": "a24f514e24fac543ee980b761066d25f7845322a",
  "block": 19012835,
  "trx_in_block": 33,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2018-01-15T23:50:15",
  "op": [
    "vote",
    {
      "voter": "marnee",
      "author": "danielkdewar",
      "permlink": "the-perfect-description-of-blockchain-technology-why-it-s-becoming-more-simple",
      "weight": 10000
    }
  ]
}
2018/01/15 23:49:36
parent author
parent permlinkamatuer
authormarnee
permlinkjoining-the-southern-arizona-ham-mesh-network
titleJoining the Southern Arizona Ham Mesh Network
body# Why am I doing this? I am into amateur radio. I have a [Technician Level](http://www.arrl.org/licensing-education-training) license, which means I can transmit on certain radio bands, including the bands used for high-speed data, like WiFi. We can use the unlicensed spectrum, like everyone else, and we can use special frequency bands allocated only to licensed Hams. This means we can get away from the noisy parts of the spectrum, and setup our own networks. It is common to setup a WiFi network on the Ham bands for emergency communications. Sometimes Hams use these networks to do emergency support on long distance trail races, like 24 Hours in the Old Pueblo, to maintain region-wide mesh networks in the case of power outages or major network outages, and lots of other things. You can read more about Ham radio on the [ARRL site.](http://www.arrl.org/) Southern Arizona has a really big mesh network using [AREDN -- Amateur Radio Emergency Data Network](https://www.aredn.org/about-us). With a bit of equipment and firmware I can join this network. This is how I did it. # Getting started If you are interested in getting involved, or in Ham radio in general, the best place to start is joining your local Ham radio club where you will probably meet lots of [Elmers](http://www.eham.net/articles/29423) that can help you get started. This is how I did it. I belong to OVARC (Oro Valley Amateur Radio Club). Here I met an [Elmer](http://www.eham.net/articles/29423) who is highly active in the local Emergency Communication community. Through him I was able to get a pre-configured radio that I just needed to connect to an antenna and point at one of the tallest nodes in the city. I was even able to get a (free) used antenna from the Pima County Emergency Communication and Operations center. They were replacing some equipment and just giving away the flat panel antennas they were using. # What I used * [Ubiquiti Bullet M2 Titanium outdoor radio](https://www.ubnt.com/airmax/bulletm/) * A used flat panel, directional antenna * Two ethernet cables * Linksys WAP54G wireless access point * A laptop with an Ethernet connection ## The Ubiquity Bullet M2 Titanium outdoor radio I got a [Bullet M2 Titanium](https://www.ubnt.com/airmax/bulletm/) pre-configured with the [AREDN software](https://www.aredn.org/about-us) from a Ham in the OVARC Ham club, ready to go with the configuration for the [Southern Arizona mesh network.](https://sites.google.com/site/k7uazclub/mesh-network) If you don't have a pre-configured Bullet, or your AREDN firmware is old, you can flash the firmware yourself by connecting your laptop to the Bullet with an Ethernet cable. You can download the latest version of the AREDN software [here](https://www.aredn.org/content/software). Scroll down to find the firmware for your radio. Then you can use the admin page for the Bullet to upload the new firmware. How you do this will depend on what is already installed on your Bullet. It does come with the airOS interface and the AREDN firmware comes with an admin interface where you can install and patch firmware. ## The flat panel antenna I was lucky and was able to get a free antenna from the Pima County Emergency Communication and Operations center. If you know enough Hams you can probably either get a used antenna or borrow one. I am using a flat panel antenna, which means I have to point it directly at another node in order to pick up a signal. I don't know the brand but it is basically [this](http://www.l-com.com/wireless-antenna-24-ghz-flat-panel-directional-antennas) and it works just great. I had to do some modifications to get the Bullet installed. It had previously been used in a video system, so it had a plastic enclosure with an Ethernet controller inside. I removed the top of the enclosure, and the controller inside, and was then able to screw in the Bullet, like this. ![Imgur](https://i.imgur.com/El63k6l.jpg) In order to make this stable, I should remove the rest of the enclosure and use the elbow connector that came with the Bullet. The Bullet is supposed to be weather proof, so I should not need to put it in an enclosure (the weather in Southern Arizona is pretty mild most of the time, too). Pro Tip: It took me a while to figure this out. There is a black cap with a rubber collar that you can wrap around the Ethernet cable. You can take out the rubber collar and open it up to wrap around the Ethernet cable and then screw the pieces together like this: ![Imgur](https://i.imgur.com/hDazMJx.jpg) ## The Wireless Access Point To make it easier to get my computers on the mesh network, I decided to plug in one of the Linksys WAP54G wireless access points I got for a really good deal at the OVARC Hamfest. After I configured my WAP54G, I just connected it to the LAN port on the Bullet POE box. Now I can put any of my computers on the network. I plan on hooking up a Raspberry Pi 3 and using it to serve an application I have in mind. Note: for some reason my WAP54G stops working and I have to cycle power to the WAP, so this might not be a long-term solution. # Putting it all together This is my setup. Now I just need to figure out how to mount this outside with line of sight to the central node (on the 5151 Broadway building). ![Imgur](https://i.imgur.com/kg2TnuP.jpg) # What's next? I want to build a messaging application that also use IPFS that can be used on the mesh network. I will probably use a Raspberry Pi 3 as the server.
json metadata{"tags":["amatuer","radio","mesh","network","technology"],"image":["https://i.imgur.com/El63k6l.jpg","https://i.imgur.com/hDazMJx.jpg","https://i.imgur.com/kg2TnuP.jpg"],"links":["http://www.arrl.org/licensing-education-training","http://www.arrl.org/","https://www.aredn.org/about-us","http://www.eham.net/articles/29423","https://www.ubnt.com/airmax/bulletm/","https://sites.google.com/site/k7uazclub/mesh-network","https://www.aredn.org/content/software","http://www.l-com.com/wireless-antenna-24-ghz-flat-panel-directional-antennas"],"app":"steemit/0.1","format":"markdown"}
Transaction InfoBlock #19012822/Trx fea6970825f4d0113e2a29662667816ac7f944da
View Raw JSON Data
{
  "trx_id": "fea6970825f4d0113e2a29662667816ac7f944da",
  "block": 19012822,
  "trx_in_block": 25,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2018-01-15T23:49:36",
  "op": [
    "comment",
    {
      "parent_author": "",
      "parent_permlink": "amatuer",
      "author": "marnee",
      "permlink": "joining-the-southern-arizona-ham-mesh-network",
      "title": "Joining the Southern Arizona Ham Mesh Network",
      "body": "# Why am I doing this?\n\nI am into amateur radio. I have a [Technician Level](http://www.arrl.org/licensing-education-training) license, which means I can transmit on certain radio bands, including the bands used for high-speed data, like WiFi. We can use the unlicensed spectrum, like everyone else, and we can use special frequency bands allocated only to licensed Hams. This means we can get away from the noisy parts of the spectrum, and setup our own networks.\n\nIt is common to setup a WiFi network on the Ham bands for emergency communications. Sometimes Hams use these networks to do emergency support on long distance trail races, like 24 Hours in the Old Pueblo, to maintain region-wide mesh networks in the case of power outages or major network outages, and lots of other things.\n\nYou can read more about Ham radio on the [ARRL site.](http://www.arrl.org/)\n\nSouthern Arizona has a really big mesh network using [AREDN -- Amateur Radio Emergency Data Network](https://www.aredn.org/about-us). With a bit of equipment and firmware I can join this network. This is how I did it.\n\n# Getting started\n\nIf you are interested in getting involved, or in Ham radio in general, the best place to start is joining your local Ham radio club where you will probably meet lots of [Elmers](http://www.eham.net/articles/29423) that can help you get started. This is how I did it. I belong to OVARC (Oro Valley Amateur Radio Club). Here I met an [Elmer](http://www.eham.net/articles/29423) who is highly active in the local Emergency Communication community. Through him I was able to get a pre-configured radio that I just needed to connect to an antenna and point at one of the tallest nodes in the city. I was even able to get a (free) used antenna from the Pima County Emergency Communication and Operations center. They were replacing some equipment and just giving away the flat panel antennas they were using. \n\n# What I used\n\n* [Ubiquiti Bullet M2 Titanium outdoor radio](https://www.ubnt.com/airmax/bulletm/)\n* A used flat panel, directional antenna\n* Two ethernet cables\n* Linksys WAP54G wireless access point\n* A laptop with an Ethernet connection\n\n## The Ubiquity Bullet M2 Titanium outdoor radio\n\nI got a [Bullet M2 Titanium](https://www.ubnt.com/airmax/bulletm/) pre-configured with the [AREDN software](https://www.aredn.org/about-us) from a Ham in the OVARC Ham club, ready to go with the configuration for the [Southern Arizona mesh network.](https://sites.google.com/site/k7uazclub/mesh-network)\n\nIf you don't have a pre-configured Bullet, or your AREDN firmware is old, you can flash the firmware yourself by connecting your laptop to the Bullet with an Ethernet cable. You can download the latest version of the AREDN software [here](https://www.aredn.org/content/software). Scroll down to find the firmware for your radio. Then you can use the admin page for the Bullet to upload the new firmware. How you do this will depend on what is already installed on your Bullet. It does come with the airOS interface and the AREDN firmware comes with an admin interface where you can install and patch firmware.\n\n## The flat panel antenna\n\nI was lucky and was able to get a free antenna from the Pima County Emergency Communication and Operations center. If you know enough Hams you can probably either get a used antenna or borrow one. I am using a flat panel antenna, which means I have to point it directly at another node in order to pick up a signal.\n\nI don't know the brand but it is basically [this](http://www.l-com.com/wireless-antenna-24-ghz-flat-panel-directional-antennas) and it works just great.\n\nI had to do some modifications to get the Bullet installed. It had previously been used in a video system, so it had a plastic enclosure with an Ethernet controller inside. I removed the top of the enclosure, and the controller inside, and was then able to screw in the Bullet, like this.\n\n![Imgur](https://i.imgur.com/El63k6l.jpg)\n\nIn order to make this stable, I should remove the rest of the enclosure and use the elbow connector that came with the Bullet.\n\nThe Bullet is supposed to be weather proof, so I should not need to put it in an enclosure (the weather in Southern Arizona is pretty mild most of the time, too).\n\nPro Tip: It took me a while to figure this out. There is a black cap with a rubber collar that you can wrap around the Ethernet cable. You can take out the rubber collar and open it up to wrap around the Ethernet cable and then screw the pieces together like this:\n\n![Imgur](https://i.imgur.com/hDazMJx.jpg)\n\n## The Wireless Access Point\n\nTo make it easier to get my computers on the mesh network, I decided to plug in one of the Linksys WAP54G wireless access points I got for a really good deal at the OVARC Hamfest.\n\nAfter I configured my WAP54G, I just connected it to the LAN port on the Bullet POE box.\n\nNow I can put any of my computers on the network. I plan on hooking up a Raspberry Pi 3 and using it to serve an application I have in mind.\n\nNote: for some reason my WAP54G stops working and I have to cycle power to the WAP, so this might not be a long-term solution.\n\n# Putting it all together\n\nThis is my setup. Now I just need to figure out how to mount this outside with line of sight to the central node (on the 5151 Broadway building).\n\n![Imgur](https://i.imgur.com/kg2TnuP.jpg)\n\n# What's next?\n\nI want to build a messaging application that also use IPFS that can be used on the mesh network. I will probably use a Raspberry Pi 3 as the server.",
      "json_metadata": "{\"tags\":[\"amatuer\",\"radio\",\"mesh\",\"network\",\"technology\"],\"image\":[\"https://i.imgur.com/El63k6l.jpg\",\"https://i.imgur.com/hDazMJx.jpg\",\"https://i.imgur.com/kg2TnuP.jpg\"],\"links\":[\"http://www.arrl.org/licensing-education-training\",\"http://www.arrl.org/\",\"https://www.aredn.org/about-us\",\"http://www.eham.net/articles/29423\",\"https://www.ubnt.com/airmax/bulletm/\",\"https://sites.google.com/site/k7uazclub/mesh-network\",\"https://www.aredn.org/content/software\",\"http://www.l-com.com/wireless-antenna-24-ghz-flat-panel-directional-antennas\"],\"app\":\"steemit/0.1\",\"format\":\"markdown\"}"
    }
  ]
}
2018/01/13 19:46:57
required auths[]
required posting auths["marnee"]
idfollow
json["follow",{"follower":"marnee","following":"billstclair","what":["blog"]}]
Transaction InfoBlock #18950414/Trx bb573b1cafbaf3ade7d6edbdee010069e7e9eba3
View Raw JSON Data
{
  "trx_id": "bb573b1cafbaf3ade7d6edbdee010069e7e9eba3",
  "block": 18950414,
  "trx_in_block": 23,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2018-01-13T19:46:57",
  "op": [
    "custom_json",
    {
      "required_auths": [],
      "required_posting_auths": [
        "marnee"
      ],
      "id": "follow",
      "json": "[\"follow\",{\"follower\":\"marnee\",\"following\":\"billstclair\",\"what\":[\"blog\"]}]"
    }
  ]
}
marneereceived 0.539 SBD, 0.244 SP author reward for @marnee / plugging-in-elm-and-suave-with-websockets-on-net-core-2-0
2017/12/31 21:53:12
authormarnee
permlinkplugging-in-elm-and-suave-with-websockets-on-net-core-2-0
sbd payout0.539 SBD
steem payout0.000 STEEM
vesting payout397.544318 VESTS
Transaction InfoBlock #18579047/Virtual Operation #13
View Raw JSON Data
{
  "trx_id": "0000000000000000000000000000000000000000",
  "block": 18579047,
  "trx_in_block": 4294967295,
  "op_in_trx": 0,
  "virtual_op": 13,
  "timestamp": "2017-12-31T21:53:12",
  "op": [
    "author_reward",
    {
      "author": "marnee",
      "permlink": "plugging-in-elm-and-suave-with-websockets-on-net-core-2-0",
      "sbd_payout": "0.539 SBD",
      "steem_payout": "0.000 STEEM",
      "vesting_payout": "397.544318 VESTS"
    }
  ]
}
2017/12/27 02:09:54
parent authormarnee
parent permlinkplugging-in-elm-and-suave-with-websockets-on-net-core-2-0
authorjclermont
permlinkre-marnee-plugging-in-elm-and-suave-with-websockets-on-net-core-2-0-20171227t020955497z
title
bodyNice write up! I am going to follow along on my machine tomorrow, but it was a good read. I, too, have been working on Elm and F#, but I haven’t tried Suave or web sockets, so I learned a lot. Thanks!
json metadata{"tags":["technology"],"app":"steemit/0.1"}
Transaction InfoBlock #18440215/Trx d70129288f889bc1228ba82f8535c20760502df2
View Raw JSON Data
{
  "trx_id": "d70129288f889bc1228ba82f8535c20760502df2",
  "block": 18440215,
  "trx_in_block": 25,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2017-12-27T02:09:54",
  "op": [
    "comment",
    {
      "parent_author": "marnee",
      "parent_permlink": "plugging-in-elm-and-suave-with-websockets-on-net-core-2-0",
      "author": "jclermont",
      "permlink": "re-marnee-plugging-in-elm-and-suave-with-websockets-on-net-core-2-0-20171227t020955497z",
      "title": "",
      "body": "Nice write up! I am going to follow along on my machine tomorrow, but it was a good read. I, too, have been working on Elm and F#, but I haven’t tried Suave or web sockets, so I learned a lot. Thanks!",
      "json_metadata": "{\"tags\":[\"technology\"],\"app\":\"steemit/0.1\"}"
    }
  ]
}
2017/12/27 02:08:30
voterjclermont
authormarnee
permlinkplugging-in-elm-and-suave-with-websockets-on-net-core-2-0
weight10000 (100.00%)
Transaction InfoBlock #18440187/Trx 407cd0d977b7b465ffd60547c6727f2ade3875f5
View Raw JSON Data
{
  "trx_id": "407cd0d977b7b465ffd60547c6727f2ade3875f5",
  "block": 18440187,
  "trx_in_block": 28,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2017-12-27T02:08:30",
  "op": [
    "vote",
    {
      "voter": "jclermont",
      "author": "marnee",
      "permlink": "plugging-in-elm-and-suave-with-websockets-on-net-core-2-0",
      "weight": 10000
    }
  ]
}
2017/12/26 18:45:21
parent author
parent permlinktechnology
authormarnee
permlinksuave-websocket-server-with-a-continuous-feed
titleSuave websocket server with a continuous feed
body# Why am I doing this? I recently wrote a blog post using Suave websockets. It was a dead-simple example of a websocket server that just echoes back what the client sends to it. I made a new example that is just a tiny bit more useful. I made the Suave websocket server return a continuous feed of the current date and time. Exciting stuff. See my previous article, [Plugging in Elm and Suave with websockets on .NET Core 2.0](https://steemit.com/technology/@marnee/plugging-in-elm-and-suave-with-websockets-on-net-core-2-0) You can find the source code for my new and improved websocket log server on my Github repo [here](https://github.com/MarneeDear/websocket-log). # What did I change? Let's walk through a bit of the code In `let app: WebPart` I changed the route to make them more obvious when I use them in a client. ```fsharp let app : WebPart = choose [ path "/websocket/datelog" >=> handShake wsDateLog // path "/websocketWithSubprotocol" >=> handShakeWithSubprotocol (chooseSubprotocol "test") ws path "/websocketWithError/datelog" >=> handShake (wsWithErrorHandling wsDateLog) GET >=> choose [ path "/" >=> file "index.html"; browseHome ] NOT_FOUND "Found no handlers." ] ``` I changed the name of the `ws` function to distinguish it so I can have multiple routes that do different things with a websocket. I changed the name to `wsDateLog`. I changed the function `ws`, now `wsDateLog`, to a function with a forevar! loop that sends back the current date, and does this every 2 seconds. I changed `wsWithErrorHandling` to take a function that returns the data so I could I plug in any function here that I want in the future. ```fsharp let wsWithErrorHandling ws (webSocket : WebSocket) (context: HttpContext) = ``` In the `path`s I changed them to pass in the `wsDateLog` instead of `ws`. ```fsharp path "/websocket/datelog" >=> handShake wsDateLog // path "/websocketWithSubprotocol" >=> handShakeWithSubprotocol (chooseSubprotocol "test") ws path "/websocketWithError/datelog" >=> handShake (wsWithErrorHandling wsDateLog) ``` Now we are ready to roll with a fancy date sender. ![Silvrback blog image ](https://silvrback.s3.amazonaws.com/uploads/a7282ca6-6708-4868-9315-5d4fe145f071/and-you-get-a-websocket-and-you-get-a-websocket.jpg) This will work with the Elm client from the previous article, just replace the `echoServer` with one of the paths in the Suave `app`. I also have an example of where I did this in Elm, but instead of rolling all of the dates returned, I just displayed the last 10. Find that code [here](https://github.com/asthenosphere/spikes/blob/master/elm-websockets/suave-connection.elm). # Thoughts My thoughts on this are that I now have the building blocks for a websocket server that will send any kind of continuous feed to a client. This could a log feed or a status of something. I am sure there are many applications. All I have to do is replace the date calculation with, say, a call to a database and stuff like that. I don't know how I feel about the sleep bits. Probably there is a better thing to do here. As always, if you have any questions or comments please tweet me @marneedear or leave a comment.
json metadata{"tags":["technology","fsharp","programming","suave"],"users":["marneedear"],"image":["https://silvrback.s3.amazonaws.com/uploads/a7282ca6-6708-4868-9315-5d4fe145f071/and-you-get-a-websocket-and-you-get-a-websocket.jpg"],"links":["https://steemit.com/technology/@marnee/plugging-in-elm-and-suave-with-websockets-on-net-core-2-0","https://github.com/MarneeDear/websocket-log","https://github.com/asthenosphere/spikes/blob/master/elm-websockets/suave-connection.elm"],"app":"steemit/0.1","format":"markdown"}
Transaction InfoBlock #18431326/Trx b569e3c973a5dcd3b9458fccfc54f1580367af9a
View Raw JSON Data
{
  "trx_id": "b569e3c973a5dcd3b9458fccfc54f1580367af9a",
  "block": 18431326,
  "trx_in_block": 49,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2017-12-26T18:45:21",
  "op": [
    "comment",
    {
      "parent_author": "",
      "parent_permlink": "technology",
      "author": "marnee",
      "permlink": "suave-websocket-server-with-a-continuous-feed",
      "title": "Suave websocket server with a continuous feed",
      "body": "# Why am I doing this?\n\nI recently wrote a blog post using Suave websockets. It was a dead-simple example of a websocket server that just echoes back what the client sends to it. I made a new example that is just a tiny bit more useful. I made the Suave websocket server return a continuous feed of the current date and time. Exciting stuff.\n\nSee my previous article, [Plugging in Elm and Suave with websockets on .NET Core 2.0](https://steemit.com/technology/@marnee/plugging-in-elm-and-suave-with-websockets-on-net-core-2-0)\n\nYou can find the source code for my new and improved websocket log server on my Github repo [here](https://github.com/MarneeDear/websocket-log).\n\n# What did I change?\n\nLet's walk through a bit of the code\n\nIn `let app: WebPart` I changed the route to make them more obvious when I use them in a client.\n\n```fsharp\nlet app : WebPart =\n  choose [\n    path \"/websocket/datelog\" >=> handShake wsDateLog\n    // path \"/websocketWithSubprotocol\" >=> handShakeWithSubprotocol (chooseSubprotocol \"test\") ws\n    path \"/websocketWithError/datelog\" >=> handShake (wsWithErrorHandling wsDateLog)\n    GET >=> choose [ path \"/\" >=> file \"index.html\"; browseHome ]\n    NOT_FOUND \"Found no handlers.\" ]\n```\n\nI changed the name of the `ws` function to distinguish it so I can have multiple routes that do different things with a websocket. I changed the name to `wsDateLog`.\n\nI changed the function `ws`, now `wsDateLog`, to a function with a forevar! loop that sends back the current date, and does this every 2 seconds.\n\nI changed `wsWithErrorHandling` to take a function that returns the data so I could I plug in any function here that I want in the future.\n\n```fsharp\nlet wsWithErrorHandling ws (webSocket : WebSocket) (context: HttpContext) =\n```\n\nIn the `path`s I changed them to pass in the `wsDateLog` instead of `ws`.\n\n```fsharp\n    path \"/websocket/datelog\" >=> handShake wsDateLog\n    // path \"/websocketWithSubprotocol\" >=> handShakeWithSubprotocol (chooseSubprotocol \"test\") ws\n    path \"/websocketWithError/datelog\" >=> handShake (wsWithErrorHandling wsDateLog)\n```\n\nNow we are ready to roll with a fancy date sender.\n\n![Silvrback blog image ](https://silvrback.s3.amazonaws.com/uploads/a7282ca6-6708-4868-9315-5d4fe145f071/and-you-get-a-websocket-and-you-get-a-websocket.jpg)\n\nThis will work with the Elm client from the previous article, just replace the `echoServer` with one of the paths in the Suave `app`.\n\nI also have an example of where I did this in Elm, but instead of rolling all of the dates returned, I just displayed the last 10. Find that code [here](https://github.com/asthenosphere/spikes/blob/master/elm-websockets/suave-connection.elm).\n\n# Thoughts\n\nMy thoughts on this are that I now have the building blocks for a websocket server that will send any kind of continuous feed to a client. This could a log feed or a status of something. I am sure there are many applications. All I have to do is replace the date calculation with, say, a call to a database and stuff like that.\n\nI don't know how I feel about the sleep bits. Probably there is a better thing to do here.\n\nAs always, if you have any questions or comments please tweet me @marneedear or leave a comment.",
      "json_metadata": "{\"tags\":[\"technology\",\"fsharp\",\"programming\",\"suave\"],\"users\":[\"marneedear\"],\"image\":[\"https://silvrback.s3.amazonaws.com/uploads/a7282ca6-6708-4868-9315-5d4fe145f071/and-you-get-a-websocket-and-you-get-a-websocket.jpg\"],\"links\":[\"https://steemit.com/technology/@marnee/plugging-in-elm-and-suave-with-websockets-on-net-core-2-0\",\"https://github.com/MarneeDear/websocket-log\",\"https://github.com/asthenosphere/spikes/blob/master/elm-websockets/suave-connection.elm\"],\"app\":\"steemit/0.1\",\"format\":\"markdown\"}"
    }
  ]
}
2017/12/26 18:44:27
parent author
parent permlinktechnology
authormarnee
permlinksuave-websocket-server-with-a-continuous-feed
titleSuave websocket server with a continuous feed
body# Why am I doing this? I recently wrote a blog post using Suave websockets. It was a dead-simple example of a websocket server that just echoes back what the client sends to it. I made a new example that is just a tiny bit more useful. I made the Suave websocket server return a continuous feed of the current date and time. Exciting stuff. See my previous article, [Plugging in Elm and Suave with websockets on .NET Core 2.0](https://steemit.com/technology/@marnee/plugging-in-elm-and-suave-with-websockets-on-net-core-2-0) You can find the source code for my new and improved websocket log server on my Github repo [here](https://github.com/MarneeDear/websocket-log). # What did I change? Let's walk through a bit of the code In `let app: WebPart` I changed the route to make them more obvious when I use them in a client. ```fsharp let app : WebPart = choose [ path "/websocket/datelog" >=> handShake wsDateLog // path "/websocketWithSubprotocol" >=> handShakeWithSubprotocol (chooseSubprotocol "test") ws path "/websocketWithError/datelog" >=> handShake (wsWithErrorHandling wsDateLog) GET >=> choose [ path "/" >=> file "index.html"; browseHome ] NOT_FOUND "Found no handlers." ] ``` I changed the name of the `ws` function to distinguish it so I can have multiple routes that do different things with a websocket. I changed the name to `wsDateLog`. I changed the function `ws`, now `wsDateLog`, to a function with a forevar! loop that sends back the current date, and does this every 2 seconds. I changed `wsWithErrorHandling` to take a function that returns the data so I could I plug in any function here that I want in the future. ```fsharp let wsWithErrorHandling ws (webSocket : WebSocket) (context: HttpContext) = ``` In the `path`s I changed them to pass in the `wsDateLog` instead of `ws`. ```fsharp path "/websocket/datelog" >=> handShake wsDateLog // path "/websocketWithSubprotocol" >=> handShakeWithSubprotocol (chooseSubprotocol "test") ws path "/websocketWithError/datelog" >=> handShake (wsWithErrorHandling wsDateLog) ``` Now we are ready to roll with a fancy date sender. ![Silvrback blog image ](https://silvrback.s3.amazonaws.com/uploads/a7282ca6-6708-4868-9315-5d4fe145f071/and-you-get-a-websocket-and-you-get-a-websocket.jpg) This will work with the Elm client from the previous article, just replace the `echoServer` with one of the paths in the Suave `app`. I also have an example of where I did this in Elm, but instead of rolling all of the dates returned, I just displayed the last 10. Find that code [here](https://github.com/asthenosphere/spikes/blob/master/elm-websockets/suave-connection.elm). # Thoughts My thoughts on this are that I now have the building blocks for a websocket server that will send any kind of continuous feed to a client. This could a log feed or a status of something. I am sure there are many applications. All I have to do is replace the date calculation with, say, a call to a database and stuff like that. I don't know how I feel about the sleep bits. Probably there is a better thing to do here. As always, if you have any questions or comments please tweet me @marneedear or leave a comment.
json metadata{"tags":["technology","fsharp","programming","suave"],"users":["marneedear"],"image":["https://silvrback.s3.amazonaws.com/uploads/a7282ca6-6708-4868-9315-5d4fe145f071/and-you-get-a-websocket-and-you-get-a-websocket.jpg"],"links":["https://steemit.com/technology/@marnee/plugging-in-elm-and-suave-with-websockets-on-net-core-2-0","https://github.com/MarneeDear/websocket-log","https://github.com/asthenosphere/spikes/blob/master/elm-websockets/suave-connection.elm"],"app":"steemit/0.1","format":"markdown"}
Transaction InfoBlock #18431308/Trx aa6b19cfb139739373efac4202d0ffce933705aa
View Raw JSON Data
{
  "trx_id": "aa6b19cfb139739373efac4202d0ffce933705aa",
  "block": 18431308,
  "trx_in_block": 0,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2017-12-26T18:44:27",
  "op": [
    "comment",
    {
      "parent_author": "",
      "parent_permlink": "technology",
      "author": "marnee",
      "permlink": "suave-websocket-server-with-a-continuous-feed",
      "title": "Suave websocket server with a continuous feed",
      "body": "# Why am I doing this?\n\nI recently wrote a blog post using Suave websockets. It was a dead-simple example of a websocket server that just echoes back what the client sends to it. I made a new example that is just a tiny bit more useful. I made the Suave websocket server return a continuous feed of the current date and time. Exciting stuff.\n\nSee my previous article, [Plugging in Elm and Suave with websockets on .NET Core 2.0](https://steemit.com/technology/@marnee/plugging-in-elm-and-suave-with-websockets-on-net-core-2-0)\n\nYou can find the source code for my new and improved websocket log server on my Github repo [here](https://github.com/MarneeDear/websocket-log).\n\n# What did I change?\n\nLet's walk through a bit of the code\n\nIn `let app: WebPart` I changed the route to make them more obvious when I use them in a client.\n\n```fsharp\nlet app : WebPart =\n  choose [\n    path \"/websocket/datelog\" >=> handShake wsDateLog\n    // path \"/websocketWithSubprotocol\" >=> handShakeWithSubprotocol (chooseSubprotocol \"test\") ws\n    path \"/websocketWithError/datelog\" >=> handShake (wsWithErrorHandling wsDateLog)\n    GET >=> choose [ path \"/\" >=> file \"index.html\"; browseHome ]\n    NOT_FOUND \"Found no handlers.\" ]\n```\n\nI changed the name of the `ws` function to distinguish it so I can have multiple routes that do different things with a websocket. I changed the name to `wsDateLog`.\n\nI changed the function `ws`, now `wsDateLog`, to a function with a forevar! loop that sends back the current date, and does this every 2 seconds.\n\nI changed `wsWithErrorHandling` to take a function that returns the data so I could I plug in any function here that I want in the future.\n\n```fsharp\nlet wsWithErrorHandling ws (webSocket : WebSocket) (context: HttpContext) =\n```\n\nIn the `path`s I changed them to pass in the `wsDateLog` instead of `ws`.\n\n```fsharp\n    path \"/websocket/datelog\" >=> handShake wsDateLog\n    // path \"/websocketWithSubprotocol\" >=> handShakeWithSubprotocol (chooseSubprotocol \"test\") ws\n    path \"/websocketWithError/datelog\" >=> handShake (wsWithErrorHandling wsDateLog)\n```\n\nNow we are ready to roll with a fancy date sender.\n\n![Silvrback blog image ](https://silvrback.s3.amazonaws.com/uploads/a7282ca6-6708-4868-9315-5d4fe145f071/and-you-get-a-websocket-and-you-get-a-websocket.jpg)\n\nThis will work with the Elm client from the previous article, just replace the `echoServer` with one of the paths in the Suave `app`.\n\nI also have an example of where I did this in Elm, but instead of rolling all of the dates returned, I just displayed the last 10. Find that code [here](https://github.com/asthenosphere/spikes/blob/master/elm-websockets/suave-connection.elm).\n\n# Thoughts\n\nMy thoughts on this are that I now have the building blocks for a websocket server that will send any kind of continuous feed to a client. This could a log feed or a status of something. I am sure there are many applications. All I have to do is replace the date calculation with, say, a call to a database and stuff like that.\n\nI don't know how I feel about the sleep bits. Probably there is a better thing to do here.\n\nAs always, if you have any questions or comments please tweet me @marneedear or leave a comment.",
      "json_metadata": "{\"tags\":[\"technology\",\"fsharp\",\"programming\",\"suave\"],\"users\":[\"marneedear\"],\"image\":[\"https://silvrback.s3.amazonaws.com/uploads/a7282ca6-6708-4868-9315-5d4fe145f071/and-you-get-a-websocket-and-you-get-a-websocket.jpg\"],\"links\":[\"https://steemit.com/technology/@marnee/plugging-in-elm-and-suave-with-websockets-on-net-core-2-0\",\"https://github.com/MarneeDear/websocket-log\",\"https://github.com/asthenosphere/spikes/blob/master/elm-websockets/suave-connection.elm\"],\"app\":\"steemit/0.1\",\"format\":\"markdown\"}"
    }
  ]
}
marneeupvoted (100.00%) @acromott / ipkytkj8
2017/12/25 05:12:06
votermarnee
authoracromott
permlinkipkytkj8
weight10000 (100.00%)
Transaction InfoBlock #18386314/Trx ab0fbe2716608c9a4a161061965084ef63e6b41d
View Raw JSON Data
{
  "trx_id": "ab0fbe2716608c9a4a161061965084ef63e6b41d",
  "block": 18386314,
  "trx_in_block": 30,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2017-12-25T05:12:06",
  "op": [
    "vote",
    {
      "voter": "marnee",
      "author": "acromott",
      "permlink": "ipkytkj8",
      "weight": 10000
    }
  ]
}
2017/12/25 04:43:45
votermarnee
authorbustami83
permlinktutorial-creating-a-simple-program-on-the-codeigniter-framework
weight10000 (100.00%)
Transaction InfoBlock #18385748/Trx ad081a5cfbd673f93b2059360770dbe16c84379b
View Raw JSON Data
{
  "trx_id": "ad081a5cfbd673f93b2059360770dbe16c84379b",
  "block": 18385748,
  "trx_in_block": 14,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2017-12-25T04:43:45",
  "op": [
    "vote",
    {
      "voter": "marnee",
      "author": "bustami83",
      "permlink": "tutorial-creating-a-simple-program-on-the-codeigniter-framework",
      "weight": 10000
    }
  ]
}

Account Metadata

POSTING JSON METADATA
profile{"name":"Marnee Dearman","about":"Programmer and ham radio enthusiast","profile_image":"https://postimg.org/imahttps://s26.postimg.org/v26zso1d5/Couch_Profile.jpgge/44d2qxgpx/"}
JSON METADATA
profile{"name":"Marnee Dearman","about":"Programmer and ham radio enthusiast","profile_image":"https://postimg.org/imahttps://s26.postimg.org/v26zso1d5/Couch_Profile.jpgge/44d2qxgpx/"}
{
  "posting_json_metadata": {
    "profile": {
      "name": "Marnee Dearman",
      "about": "Programmer and ham radio enthusiast",
      "profile_image": "https://postimg.org/imahttps://s26.postimg.org/v26zso1d5/Couch_Profile.jpgge/44d2qxgpx/"
    }
  },
  "json_metadata": {
    "profile": {
      "name": "Marnee Dearman",
      "about": "Programmer and ham radio enthusiast",
      "profile_image": "https://postimg.org/imahttps://s26.postimg.org/v26zso1d5/Couch_Profile.jpgge/44d2qxgpx/"
    }
  }
}

Auth Keys

Owner
Single Signature
Public Keys
STM5Nj1zUJJ9ySh9kBF2Movdvrp64h64gvd3sX5yDCi2vyXdV3qA81/1
Active
Single Signature
Public Keys
STM6Lusj37LXmBTrYSZzwSPKnRuyKPUDShQTLKCBLGCPCaEU1of1w1/1
Posting
Single Signature
Public Keys
STM5BheYsZWVkffyGjbywqhPqgMfiDnACLGknd3sCKwpbDuwYgWqG1/1
Memo
STM6aQ2Kzb3qwPfPiy434uLdAcfT9RxVZs22Lo2pDsHjidYMWMXNH
{
  "owner": {
    "weight_threshold": 1,
    "account_auths": [],
    "key_auths": [
      [
        "STM5Nj1zUJJ9ySh9kBF2Movdvrp64h64gvd3sX5yDCi2vyXdV3qA8",
        1
      ]
    ]
  },
  "active": {
    "weight_threshold": 1,
    "account_auths": [],
    "key_auths": [
      [
        "STM6Lusj37LXmBTrYSZzwSPKnRuyKPUDShQTLKCBLGCPCaEU1of1w",
        1
      ]
    ]
  },
  "posting": {
    "weight_threshold": 1,
    "account_auths": [],
    "key_auths": [
      [
        "STM5BheYsZWVkffyGjbywqhPqgMfiDnACLGknd3sCKwpbDuwYgWqG",
        1
      ]
    ]
  },
  "memo": "STM6aQ2Kzb3qwPfPiy434uLdAcfT9RxVZs22Lo2pDsHjidYMWMXNH"
}

Witness Votes

0 / 30
No active witness votes.
[]