diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..132451c --- /dev/null +++ b/.editorconfig @@ -0,0 +1,240 @@ +# Remove the line below if you want to inherit .editorconfig settings from higher directories +root = true + +# all files +[*] + +charset = utf-8 + +# Indentation and spacing +indent_style = space +indent_size = 4 +tab_width = 4 + +# New line preferences +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +# C# files +[*.cs] + +file_header_template = Copyright © myCSharp 2020-2021, all rights reserved + +#### .NET Coding Conventions #### + +# Organize usings +dotnet_separate_import_directive_groups = false +dotnet_sort_system_directives_first = true + +# this. and Me. preferences +dotnet_style_qualification_for_event = true:suggestion +dotnet_style_qualification_for_field = false:suggestion +dotnet_style_qualification_for_method = true:suggestion +dotnet_style_qualification_for_property = true:suggestion + +# Language keywords vs BCL types preferences +dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion +dotnet_style_predefined_type_for_member_access = true:suggestion + +# Parentheses preferences +dotnet_style_parentheses_in_arithmetic_binary_operators = never_if_unnecessary:silent +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent + +# Modifier preferences +dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent + +# Expression-level preferences +csharp_style_deconstructed_variable_declaration = true:suggestion +csharp_style_inlined_variable_declaration = true:suggestion +csharp_style_throw_expression = true:suggestion +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_object_initializer = true:suggestion +dotnet_style_prefer_auto_properties = true:silent +dotnet_style_prefer_compound_assignment = true:suggestion +dotnet_style_prefer_conditional_expression_over_assignment = true:silent +dotnet_style_prefer_conditional_expression_over_return = true:silent +dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion +dotnet_style_prefer_inferred_tuple_names = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion + +# Field preferences +dotnet_style_readonly_field = true:suggestion + +# Parameter preferences +dotnet_code_quality_unused_parameters = all:suggestion + +#### C# Coding Conventions #### + +# var preferences +csharp_style_var_elsewhere = false:silent +csharp_style_var_for_built_in_types = false:silent +csharp_style_var_when_type_is_apparent = true:silent + +# Expression-bodied members +csharp_style_expression_bodied_accessors = true:silent +csharp_style_expression_bodied_constructors = true:silent +csharp_style_expression_bodied_indexers = true:silent +csharp_style_expression_bodied_lambdas = true:silent +csharp_style_expression_bodied_local_functions = true:silent +csharp_style_expression_bodied_methods = true:silent +csharp_style_expression_bodied_operators = true:silent +csharp_style_expression_bodied_properties = true:silent + +# Pattern matching preferences +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion +csharp_style_prefer_switch_expression = true:suggestion + +# Null-checking preferences +csharp_style_conditional_delegate_call = true:suggestion + +# Modifier preferences +csharp_prefer_static_local_function = true:suggestion +csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async + +# Code-block preferences +csharp_prefer_braces = false:silent +csharp_prefer_simple_using_statement = true:suggestion + +# Expression-level preferences +csharp_prefer_simple_default_expression = true:suggestion +csharp_style_pattern_local_over_anonymous_function = true:suggestion +csharp_style_prefer_index_operator = true:suggestion +csharp_style_prefer_range_operator = true:suggestion +csharp_style_unused_value_assignment_preference = discard_variable:suggestion +csharp_style_unused_value_expression_statement_preference = discard_variable:silent + +# 'using' directive preferences +csharp_using_directive_placement = outside_namespace:silent + +#### C# Formatting Rules #### + +# New line preferences +csharp_new_line_before_catch = true +csharp_new_line_before_else = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_open_brace = all +csharp_new_line_between_query_expression_clauses = true + +# Indentation preferences +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents = true +csharp_indent_case_contents_when_block = true +csharp_indent_labels = one_less_than_current +csharp_indent_switch_labels = true + +# Space preferences +csharp_space_after_cast = false +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_after_comma = true +csharp_space_after_dot = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_after_semicolon_in_for_statement = true +csharp_space_around_binary_operators = before_and_after +csharp_space_around_declaration_statements = false +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_before_comma = false +csharp_space_before_dot = false +csharp_space_before_open_square_brackets = false +csharp_space_before_semicolon_in_for_statement = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_between_square_brackets = false + +# Wrapping preferences +csharp_preserve_single_line_blocks = true +csharp_preserve_single_line_statements = true + +#### Naming styles #### + +# Naming rules + +dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion +dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface +dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i + +dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.types_should_be_pascal_case.symbols = types +dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case + +dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members +dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case + +dotnet_naming_rule.constant_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.constant_should_be_pascal_case.symbols = constant +dotnet_naming_rule.constant_should_be_pascal_case.style = pascal_case + +dotnet_naming_rule.private_or_internal_static_field_should_be_static_field.severity = suggestion +dotnet_naming_rule.private_or_internal_static_field_should_be_static_field.symbols = private_or_internal_static_field +dotnet_naming_rule.private_or_internal_static_field_should_be_static_field.style = static_field + +dotnet_naming_rule.private_or_internal_field_should_be_instance_field.severity = suggestion +dotnet_naming_rule.private_or_internal_field_should_be_instance_field.symbols = private_or_internal_field +dotnet_naming_rule.private_or_internal_field_should_be_instance_field.style = instance_field + +# Symbol specifications + +dotnet_naming_symbols.interface.applicable_kinds = interface +dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.interface.required_modifiers = + +dotnet_naming_symbols.private_or_internal_field.applicable_kinds = field +dotnet_naming_symbols.private_or_internal_field.applicable_accessibilities = internal, private, private_protected +dotnet_naming_symbols.private_or_internal_field.required_modifiers = + +dotnet_naming_symbols.private_or_internal_static_field.applicable_kinds = field +dotnet_naming_symbols.private_or_internal_static_field.applicable_accessibilities = internal, private, private_protected +dotnet_naming_symbols.private_or_internal_static_field.required_modifiers = static + +dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum +dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.types.required_modifiers = + +dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method +dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.non_field_members.required_modifiers = + +dotnet_naming_symbols.constant.applicable_kinds = field +dotnet_naming_symbols.constant.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.constant.required_modifiers = const + +# Naming styles + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case + +dotnet_naming_style.begins_with_i.required_prefix = I +dotnet_naming_style.begins_with_i.required_suffix = +dotnet_naming_style.begins_with_i.word_separator = +dotnet_naming_style.begins_with_i.capitalization = pascal_case + +dotnet_naming_style.static_field.required_prefix = s_ +dotnet_naming_style.static_field.required_suffix = +dotnet_naming_style.static_field.word_separator = +dotnet_naming_style.static_field.capitalization = camel_case + +dotnet_naming_style.instance_field.required_prefix = _ +dotnet_naming_style.instance_field.required_suffix = +dotnet_naming_style.instance_field.word_separator = +dotnet_naming_style.instance_field.capitalization = camel_case + +# RCS1096: Convert 'HasFlag' call to bitwise operation (or vice versa). +dotnet_diagnostic.RCS1096.severity = none diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..fac6cb5 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,46 @@ +name: NETCore + +on: + push: + branches: + - master + pull_request: + types: [closed] + branches: + - master + +env: + BuildConfig: Release + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 # avoid shallow clone so nbgv can do its work. + + - uses: dotnet/nbgv@master # https://github.com/dotnet/nbgv + id: nbgv + + - name: Versioning + run: echo ${{ steps.nbgv.outputs.SemVer2 }} + + - name: Build with dotnet + run: dotnet build + --configuration ${{ env.BuildConfig }} + /p:Version=${{ steps.nbgv.outputs.AssemblyVersion }} + + - name: Test with dotnet + run: dotnet test + + - name: Pack NuGet + run: dotnet pack + --configuration ${{ env.BuildConfig }} + /p:Version=${{ steps.nbgv.outputs.NuGetPackageVersion }} + + - name: Push to NuGet + run: dotnet nuget push **/*.nupkg + --api-key ${{ secrets.NUGET_DEPLOY_KEY }} + --source https://api.nuget.org/v3/index.json + --no-symbols 1 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..96b845c --- /dev/null +++ b/.gitignore @@ -0,0 +1,320 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ +##**/Properties/launchSettings.json + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..3a9d663 --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,18 @@ + + + 2.12 + true + MyCSharp.de, Benjamin Abt, Günther Foidl and Contributors + https://github.com/mycsharp/HttpUserAgentParser + MIT + en-US + $(MSBuildProjectName.Contains('Test')) + HTTP User Agent Parser for .NET + true + preview + enable + true + true + embedded + + diff --git a/MyCSharp.HttpUserAgentParser.sln b/MyCSharp.HttpUserAgentParser.sln new file mode 100644 index 0000000..defb60f --- /dev/null +++ b/MyCSharp.HttpUserAgentParser.sln @@ -0,0 +1,88 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.31306.274 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{008A2BAB-78B4-42EB-A5D4-DE434438CEF0}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MyCSharp.HttpUserAgentParser.AspNetCore", "src\MyCSharp.HttpUserAgentParser.AspNetCore\MyCSharp.HttpUserAgentParser.AspNetCore.csproj", "{45927CF7-1BF4-479B-BBAA-8AD9CA901AE4}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MyCSharp.HttpUserAgentParser", "src\MyCSharp.HttpUserAgentParser\MyCSharp.HttpUserAgentParser.csproj", "{3357BEC0-8216-409E-A539-F9A71DBACB81}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MyCSharp.HttpUserAgentParser.UnitTests", "tests\MyCSharp.HttpUserAgentParser.UnitTests\MyCSharp.HttpUserAgentParser.UnitTests.csproj", "{F16697F7-74B4-441D-A0C0-1A0572AC3AB0}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MyCSharp.HttpUserAgentParser.AspNetCore.UnitTests", "tests\MyCSharp.HttpUserAgentParser.AspNetCore.UnitTests\MyCSharp.HttpUserAgentParser.AspNetCore.UnitTests.csproj", "{75960783-8BF9-479C-9ECF-E9653B74C9A2}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{F54C9296-4EF7-40F0-9F20-F23A2270ABC9}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MyCSharp.HttpUserAgentParser.MemoryCache", "src\MyCSharp.HttpUserAgentParser.MemoryCache\MyCSharp.HttpUserAgentParser.MemoryCache.csproj", "{3C8CCD44-F47C-4624-8997-54C42F02E376}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MyCSharp.HttpUserAgentParser.MemoryCache.UnitTests", "tests\MyCSharp.HttpUserAgentParser.MemoryCache.UnitTests\MyCSharp.HttpUserAgentParser.MemoryCache.UnitTests.csproj", "{39FC1EC2-2AD3-411F-A545-AB6CCB94FB7E}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "_", "_", "{5738CE0D-5E6E-47CB-BFF5-08F45A2C33AD}" + ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig + .gitignore = .gitignore + .github\workflows\ci.yml = .github\workflows\ci.yml + Directory.Build.props = Directory.Build.props + LICENSE = LICENSE + NuGet.config = NuGet.config + README.md = README.md + version.json = version.json + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "perf", "perf", "{FAAD18A0-E1B8-448D-B611-AFBDA8A89808}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MyCSharp.HttpUserAgentParser.Benchmarks", "perf\MyCSharp.HttpUserAgentParser.Benchmarks\MyCSharp.HttpUserAgentParser.Benchmarks.csproj", "{A0D213E9-6408-46D1-AFAF-5096C2F6E027}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {45927CF7-1BF4-479B-BBAA-8AD9CA901AE4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {45927CF7-1BF4-479B-BBAA-8AD9CA901AE4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {45927CF7-1BF4-479B-BBAA-8AD9CA901AE4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {45927CF7-1BF4-479B-BBAA-8AD9CA901AE4}.Release|Any CPU.Build.0 = Release|Any CPU + {3357BEC0-8216-409E-A539-F9A71DBACB81}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3357BEC0-8216-409E-A539-F9A71DBACB81}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3357BEC0-8216-409E-A539-F9A71DBACB81}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3357BEC0-8216-409E-A539-F9A71DBACB81}.Release|Any CPU.Build.0 = Release|Any CPU + {F16697F7-74B4-441D-A0C0-1A0572AC3AB0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F16697F7-74B4-441D-A0C0-1A0572AC3AB0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F16697F7-74B4-441D-A0C0-1A0572AC3AB0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F16697F7-74B4-441D-A0C0-1A0572AC3AB0}.Release|Any CPU.Build.0 = Release|Any CPU + {75960783-8BF9-479C-9ECF-E9653B74C9A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {75960783-8BF9-479C-9ECF-E9653B74C9A2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {75960783-8BF9-479C-9ECF-E9653B74C9A2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {75960783-8BF9-479C-9ECF-E9653B74C9A2}.Release|Any CPU.Build.0 = Release|Any CPU + {3C8CCD44-F47C-4624-8997-54C42F02E376}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3C8CCD44-F47C-4624-8997-54C42F02E376}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3C8CCD44-F47C-4624-8997-54C42F02E376}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3C8CCD44-F47C-4624-8997-54C42F02E376}.Release|Any CPU.Build.0 = Release|Any CPU + {39FC1EC2-2AD3-411F-A545-AB6CCB94FB7E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {39FC1EC2-2AD3-411F-A545-AB6CCB94FB7E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {39FC1EC2-2AD3-411F-A545-AB6CCB94FB7E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {39FC1EC2-2AD3-411F-A545-AB6CCB94FB7E}.Release|Any CPU.Build.0 = Release|Any CPU + {A0D213E9-6408-46D1-AFAF-5096C2F6E027}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A0D213E9-6408-46D1-AFAF-5096C2F6E027}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A0D213E9-6408-46D1-AFAF-5096C2F6E027}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A0D213E9-6408-46D1-AFAF-5096C2F6E027}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {45927CF7-1BF4-479B-BBAA-8AD9CA901AE4} = {008A2BAB-78B4-42EB-A5D4-DE434438CEF0} + {3357BEC0-8216-409E-A539-F9A71DBACB81} = {008A2BAB-78B4-42EB-A5D4-DE434438CEF0} + {F16697F7-74B4-441D-A0C0-1A0572AC3AB0} = {F54C9296-4EF7-40F0-9F20-F23A2270ABC9} + {75960783-8BF9-479C-9ECF-E9653B74C9A2} = {F54C9296-4EF7-40F0-9F20-F23A2270ABC9} + {3C8CCD44-F47C-4624-8997-54C42F02E376} = {008A2BAB-78B4-42EB-A5D4-DE434438CEF0} + {39FC1EC2-2AD3-411F-A545-AB6CCB94FB7E} = {F54C9296-4EF7-40F0-9F20-F23A2270ABC9} + {A0D213E9-6408-46D1-AFAF-5096C2F6E027} = {FAAD18A0-E1B8-448D-B611-AFBDA8A89808} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {E8B0C994-0BF2-4692-9E22-E48B265B2804} + EndGlobalSection +EndGlobal diff --git a/NuGet.config b/NuGet.config new file mode 100644 index 0000000..5368ac4 --- /dev/null +++ b/NuGet.config @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/README.md b/README.md index adf48f3..2190b88 100644 --- a/README.md +++ b/README.md @@ -1 +1,155 @@ -# MyCSharp.HttpUserAgentParser \ No newline at end of file +# MyCSharp.HttpUserAgentParser + +Parsing HTTP User Agents with .NET + +## NuGet + +| NuGet | +|-| +| [![MyCSharp.HttpUserAgentParser](https://img.shields.io/nuget/v/MyCSharp.HttpUserAgentParser.svg?logo=nuget&label=MyCSharp.HttpUserAgentParser)](https://www.nuget.org/packages/MyCSharp.HttpUserAgentParser) | +| [![MyCSharp.HttpUserAgentParser](https://img.shields.io/nuget/v/MyCSharp.HttpUserAgentParser.MemoryCache.svg?logo=nuget&label=MyCSharp.HttpUserAgentParser.MemoryCache)](https://www.nuget.org/packages/MyCSharp.HttpUserAgentParser.MemoryCache)| `dotnet add package MyCSharp.HttpUserAgentParser.MemoryCach.MemoryCache` | +| [![MyCSharp.HttpUserAgentParser.AspNetCore](https://img.shields.io/nuget/v/MyCSharp.HttpUserAgentParser.AspNetCore.svg?logo=nuget&label=MyCSharp.HttpUserAgentParser.AspNetCore)](https://www.nuget.org/packages/MyCSharp.HttpUserAgentParser.AspNetCore) | `dotnet add package MyCSharp.HttpUserAgentParser.AspNetCore` | + + +## Usage + +```csharp +string userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36"; +HttpUserAgentInformation info = HttpUserAgentParser.Parse(userAgent); // alias HttpUserAgentInformation.Parse() +``` +returns +```csharp +UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36" +Type = HttpUserAgentType.Browser +Platform = { + Name = "Windows 10", + PlatformType = HttpUserAgentPlatformType.Windows +} +Name = "Chrome" +Version = "90.0.4430.212" +MobileDeviceType = null +``` + +### Dependency Injection and Caching + +For dependency injection mechanisms, the `IHttpUserAgentParserProvider` interface exists, for which built-in or custom caching mechanisms can be used. The use is always: + +```csharp +private IHttpUserAgentParserProvider _parser; +public void MyMethod(string userAgent) +{ + HttpUserAgentInformation info = _parser.Parse(userAgent); +} +``` + +If no cache is required but dependency injection is still desired, the default cache provider can simply be used. This registers the `HttpUserAgentParserDefaultProvider`, which does not cache at all. + +```csharp +public void ConfigureServices(IServiceCollection services) +{ + services.AddHttpUserAgentParser(); // uses HttpUserAgentParserDefaultProvider and does not cache +} +``` + +Likewise, an In Process Cache mechanism is provided, based on a `ConcurrentDictionary`. + +```csharp +public void ConfigureServices(IServiceCollection services) +{ + services.AddHttpUserAgentCachedParser(); // uses `HttpUserAgentParserCachedProvider` + // or + // services.AddHttpUserAgentParser(); +} +``` + + This is especially recommended for tests. For web applications, the `IMemoryCache` implementation should be used, which offers a timed expiration of the entries. + +The package [MyCSharp.HttpUserAgentParser.MemoryCache](https://www.nuget.org/packages/MyCSharp.HttpUserAgentParser.MemoryCache) is required to use the IMemoryCache. This enables the registration of the `IMemoryCache` implementation: + + +```csharp +public void ConfigureServices(IServiceCollection services) +{ + services.AddHttpUserAgentMemoryCachedParser(); + + // or use options + + services.AddHttpUserAgentMemoryCachedParser(options => + { + options.CacheEntryOptions.SlidingExpiration = TimeSpan.FromMinutes(60); // default is 1 day + + // limit the total entries in the MemoryCache + // each unique user agent string counts as one entry + options.CacheOptions.SizeLimit = 1024; // default is 256 + }); +} +``` + +### ASP.NET Core + +For ASP.NET Core applications, an accessor pattern (`IHttpUserAgentParserAccessor`) implementation can be registered additionally that independently retrieves the user agent based on the `HttpContextAccessor`. This requires the package [MyCSharp.HttpUserAgentParser.AspNetCore](https://www.nuget.org/packages/MyCSharp.HttpUserAgentParser.AspNetCore) + +```csharp +public void ConfigureServices(IServiceCollection services) +{ + services.AddHttpUserAgentParserAccessor(); // registers IHttpUserAgentParserAccessor +} +``` + +Now you can use + +```csharp +public void MyMethod(IHttpUserAgentParserAccessor parserAccessor) +{ + HttpUserAgentInformation info = parserAccessor.Get(); +} +``` + +## Benchmark + +```sh +BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19042 +AMD Ryzen 9 5950X, 1 CPU, 32 logical and 16 physical cores +.NET Core SDK=5.0.300-preview.21228.15 + [Host] : .NET Core 5.0.5 (CoreCLR 5.0.521.16609, CoreFX 5.0.521.16609), X64 RyuJIT + DefaultJob : .NET Core 5.0.5 (CoreCLR 5.0.521.16609, CoreFX 5.0.521.16609), X64 RyuJIT +``` + +| Method | Mean | Error | StdDev | Gen 0 | Gen 1 | Gen 2 | Allocated | +|-------------------- |------------:|----------:|----------:|--------:|-------:|------:|----------:| +| 'UA Parser' | 3,238.59 us | 27.435 us | 25.663 us | 7.8125 | - | - | 168225 B | +| UserAgentService | 391.11 us | 5.126 us | 4.795 us | 35.1563 | 3.4180 | - | 589664 B | +| HttpUserAgentParser | 67.07 us | 0.740 us | 0.693 us | - | - | - | 848 B | + +## Disclaimer + +This library is inspired by [UserAgentService by DannyBoyNg](https://github.com/DannyBoyNg/UserAgentService) and contains optimizations for our requirements on [myCSharp.de](https://mycsharp.de). +We decided to fork the project, because we want a general restructuring with corresponding breaking changes. + +## Maintained + +by [@BenjaminAbt](https://github.com/BenjaminAbt) and [@gfoidl](https://github.com/gfoidl) + +## License + +MIT License + +Copyright (c) 2021 MyCSharp + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/perf/MyCSharp.HttpUserAgentParser.Benchmarks/ExternalCode/UserAgentServiceUserAgent.cs b/perf/MyCSharp.HttpUserAgentParser.Benchmarks/ExternalCode/UserAgentServiceUserAgent.cs new file mode 100644 index 0000000..661a3e8 --- /dev/null +++ b/perf/MyCSharp.HttpUserAgentParser.Benchmarks/ExternalCode/UserAgentServiceUserAgent.cs @@ -0,0 +1,334 @@ +// Copyright © myCSharp 2020-2021, all rights reserved + +using System; +using System.Collections.Generic; +using System.Text.RegularExpressions; + +namespace MyCSharp.HttpUserAgentParser.Benchmarks.ExternalCode +{ + /// + /// from https://raw.githubusercontent.com/DannyBoyNg/UserAgentService/master/UserAgentService/UserAgent.cs + /// as copy because ctor is internal + /// + internal class UserAgentServiceUserAgent + { + internal static Dictionary platforms = new() + { + { "windows nt 10.0", "Windows 10" }, + { "windows nt 6.3", "Windows 8.1" }, + { "windows nt 6.2", "Windows 8" }, + { "windows nt 6.1", "Windows 7" }, + { "windows nt 6.0", "Windows Vista" }, + { "windows nt 5.2", "Windows 2003" }, + { "windows nt 5.1", "Windows XP" }, + { "windows nt 5.0", "Windows 2000" }, + { "windows nt 4.0", "Windows NT 4.0" }, + { "winnt4.0", "Windows NT 4.0" }, + { "winnt 4.0", "Windows NT" }, + { "winnt", "Windows NT" }, + { "windows 98", "Windows 98" }, + { "win98", "Windows 98" }, + { "windows 95", "Windows 95" }, + { "win95", "Windows 95" }, + { "windows phone", "Windows Phone" }, + { "windows", "Unknown Windows OS" }, + { "android", "Android" }, + { "blackberry", "BlackBerry" }, + { "iphone", "iOS" }, + { "ipad", "iOS" }, + { "ipod", "iOS" }, + { "os x", "Mac OS X" }, + { "ppc mac", "Power PC Mac" }, + { "freebsd", "FreeBSD" }, + { "ppc", "Macintosh" }, + { "linux", "Linux" }, + { "debian", "Debian" }, + { "sunos", "Sun Solaris" }, + { "beos", "BeOS" }, + { "apachebench", "ApacheBench" }, + { "aix", "AIX" }, + { "irix", "Irix" }, + { "osf", "DEC OSF" }, + { "hp-ux", "HP-UX" }, + { "netbsd", "NetBSD" }, + { "bsdi", "BSDi" }, + { "openbsd", "OpenBSD" }, + { "gnu", "GNU/Linux" }, + { "unix", "Unknown Unix OS" }, + { "symbian", "Symbian OS" }, + }; + + internal static Dictionary browsers = new() + { + { "OPR", "Opera" }, + { "Flock", "Flock" }, + { "Edge", "Edge" }, + { "Edg", "Edge" }, + { "Chrome", "Chrome" }, + { "Opera.*?Version", "Opera" }, + { "Opera", "Opera" }, + { "MSIE", "Internet Explorer" }, + { "Internet Explorer", "Internet Explorer" }, + { "Trident.* rv", "Internet Explorer" }, + { "Shiira", "Shiira" }, + { "Firefox", "Firefox" }, + { "Chimera", "Chimera" }, + { "Phoenix", "Phoenix" }, + { "Firebird", "Firebird" }, + { "Camino", "Camino" }, + { "Netscape", "Netscape" }, + { "OmniWeb", "OmniWeb" }, + { "Safari", "Safari" }, + { "Mozilla", "Mozilla" }, + { "Konqueror", "Konqueror" }, + { "icab", "iCab" }, + { "Lynx", "Lynx" }, + { "Links", "Links" }, + { "hotjava", "HotJava" }, + { "amaya", "Amaya" }, + { "IBrowse", "IBrowse" }, + { "Maxthon", "Maxthon" }, + { "Ubuntu", "Ubuntu Web Browser" }, + { "Vivaldi", "Vivaldi" }, + }; + + internal static Dictionary mobiles = new() + { + // Legacy + { "mobileexplorer", "Mobile Explorer" }, + { "palmsource", "Palm" }, + { "palmscape", "Palmscape" }, + // Phones and Manufacturers + { "motorola", "Motorola" }, + { "nokia", "Nokia" }, + { "palm", "Palm" }, + { "iphone", "Apple iPhone" }, + { "ipad", "iPad" }, + { "ipod", "Apple iPod Touch" }, + { "sony", "Sony Ericsson" }, + { "ericsson", "Sony Ericsson" }, + { "blackberry", "BlackBerry" }, + { "cocoon", "O2 Cocoon" }, + { "blazer", "Treo" }, + { "lg", "LG" }, + { "amoi", "Amoi" }, + { "xda", "XDA" }, + { "mda", "MDA" }, + { "vario", "Vario" }, + { "htc", "HTC" }, + { "samsung", "Samsung" }, + { "sharp", "Sharp" }, + { "sie-", "Siemens" }, + { "alcatel", "Alcatel" }, + { "benq", "BenQ" }, + { "ipaq", "HP iPaq" }, + { "mot-", "Motorola" }, + { "playstation portable", "PlayStation Portable" }, + { "playstation 3", "PlayStation 3" }, + { "playstation vita", "PlayStation Vita" }, + { "hiptop", "Danger Hiptop" }, + { "nec-", "NEC" }, + { "panasonic", "Panasonic" }, + { "philips", "Philips" }, + { "sagem", "Sagem" }, + { "sanyo", "Sanyo" }, + { "spv", "SPV" }, + { "zte", "ZTE" }, + { "sendo", "Sendo" }, + { "nintendo dsi", "Nintendo DSi" }, + { "nintendo ds", "Nintendo DS" }, + { "nintendo 3ds", "Nintendo 3DS" }, + { "wii", "Nintendo Wii" }, + { "open web", "Open Web" }, + { "openweb", "OpenWeb" }, + // Operating Systems + { "android", "Android" }, + { "symbian", "Symbian" }, + { "SymbianOS", "SymbianOS" }, + { "elaine", "Palm" }, + { "series60", "Symbian S60" }, + { "windows ce", "Windows CE" }, + // Browsers + { "obigo", "Obigo" }, + { "netfront", "Netfront Browser" }, + { "openwave", "Openwave Browser" }, + { "mobilexplorer", "Mobile Explorer" }, + { "operamini", "Opera Mini" }, + { "opera mini", "Opera Mini" }, + { "opera mobi", "Opera Mobile" }, + { "fennec", "Firefox Mobile" }, + // Other + { "digital paths", "Digital Paths" }, + { "avantgo", "AvantGo" }, + { "xiino", "Xiino" }, + { "novarra", "Novarra Transcoder" }, + { "vodafone", "Vodafone" }, + { "docomo", "NTT DoCoMo" }, + { "o2", "O2" }, + // Fallback + { "mobile", "Generic Mobile" }, + { "wireless", "Generic Mobile" }, + { "j2me", "Generic Mobile" }, + { "midp", "Generic Mobile" }, + { "cldc", "Generic Mobile" }, + { "up.link", "Generic Mobile" }, + { "up.browser", "Generic Mobile" }, + { "smartphone", "Generic Mobile" }, + { "cellphone", "Generic Mobile" }, + }; + + internal static Dictionary robots = new() + { + { "googlebot", "Googlebot" }, + { "msnbot", "MSNBot" }, + { "baiduspider", "Baiduspider" }, + { "bingbot", "Bing" }, + { "slurp", "Inktomi Slurp" }, + { "yahoo", "Yahoo" }, + { "ask jeeves", "Ask Jeeves" }, + { "fastcrawler", "FastCrawler" }, + { "infoseek", "InfoSeek Robot 1.0" }, + { "lycos", "Lycos" }, + { "yandex", "YandexBot" }, + { "mediapartners-google", "MediaPartners Google" }, + { "CRAZYWEBCRAWLER", "Crazy Webcrawler" }, + { "adsbot-google", "AdsBot Google" }, + { "feedfetcher-google", "Feedfetcher Google" }, + { "curious george", "Curious George" }, + { "ia_archiver", "Alexa Crawler" }, + { "MJ12bot", "Majestic-12" }, + { "Uptimebot", "Uptimebot" }, + }; + + internal string Agent = ""; + + /// + /// Gets or sets a value indicating whether this UserAgent is a browser. + /// + /// + /// true if this UserAgent is a browser; otherwise, false. + /// + public bool IsBrowser { get; set; } = false; + /// + /// Gets or sets a value indicating whether this UserAgent is a robot. + /// + /// + /// true if this UserAgent is a robot; otherwise, false. + /// + public bool IsRobot { get; set; } = false; + /// + /// Gets or sets a value indicating whether this UserAgent is a mobile device. + /// + /// + /// true if this UserAgent is a mobile device; otherwise, false. + /// + public bool IsMobile { get; set; } = false; + /// + /// Gets or sets the platform. + /// + /// + /// The platform or operating system. + /// + public string Platform { get; set; } = ""; + /// + /// Gets or sets the browser. + /// + /// + /// The browser. + /// + public string Browser { get; set; } = ""; + /// + /// Gets or sets the browser version. + /// + /// + /// The browser version. + /// + public string BrowserVersion { get; set; } = ""; + /// + /// Gets or sets the mobile device. + /// + /// + /// The mobile device. + /// + public string Mobile { get; set; } = ""; + /// + /// Gets or sets the robot. + /// + /// + /// The robot. + /// + public string Robot { get; set; } = ""; + + internal UserAgentServiceUserAgent(string? userAgentString = null) + { + if (userAgentString != null) + { + Agent = userAgentString.Trim(); + this.SetPlatform(); + if (this.SetRobot()) return; + if (this.SetBrowser()) return; + if (this.SetMobile()) return; + } + } + + internal bool SetPlatform() + { + foreach (var item in platforms) + { + if (Regex.IsMatch(Agent, $"{Regex.Escape(item.Key)}", RegexOptions.IgnoreCase)) + { + this.Platform = item.Value; + return true; + } + } + this.Platform = "Unknown Platform"; + return false; + } + + internal bool SetBrowser() + { + foreach (var item in browsers) + { + var match = Regex.Match(Agent, $@"{item.Key}.*?([0-9\.]+)", RegexOptions.IgnoreCase); + if (match.Success) + { + this.IsBrowser = true; + this.BrowserVersion = match.Groups[1].Value; + this.Browser = item.Value; + this.SetMobile(); + return true; + } + } + return false; + } + + internal bool SetRobot() + { + foreach (var item in robots) + { + if (Regex.IsMatch(Agent, $"{Regex.Escape(item.Key)}", RegexOptions.IgnoreCase)) + { + this.IsRobot = true; + this.Robot = item.Value; + this.SetMobile(); + return true; + } + } + return false; + } + + internal bool SetMobile() + { + foreach (var item in mobiles) + { + if (Agent?.IndexOf(item.Key, StringComparison.OrdinalIgnoreCase) != -1) + { + this.IsMobile = true; + this.Mobile = item.Value; + return true; + } + } + return false; + } + } +} diff --git a/perf/MyCSharp.HttpUserAgentParser.Benchmarks/MyCSharp.HttpUserAgentParser.Benchmarks.csproj b/perf/MyCSharp.HttpUserAgentParser.Benchmarks/MyCSharp.HttpUserAgentParser.Benchmarks.csproj new file mode 100644 index 0000000..bbfef02 --- /dev/null +++ b/perf/MyCSharp.HttpUserAgentParser.Benchmarks/MyCSharp.HttpUserAgentParser.Benchmarks.csproj @@ -0,0 +1,21 @@ + + + + Exe + net5.0 + true + 9.0 + full + true + + + + + + + + + + + + diff --git a/perf/MyCSharp.HttpUserAgentParser.Benchmarks/Program.cs b/perf/MyCSharp.HttpUserAgentParser.Benchmarks/Program.cs new file mode 100644 index 0000000..743334c --- /dev/null +++ b/perf/MyCSharp.HttpUserAgentParser.Benchmarks/Program.cs @@ -0,0 +1,14 @@ +// Copyright © myCSharp 2020-2021, all rights reserved + +using BenchmarkDotNet.Running; + +namespace MyCSharp.HttpUserAgentParser.Benchmarks +{ + class Program + { + static void Main() + { + BenchmarkRunner.Run(); + } + } +} diff --git a/perf/MyCSharp.HttpUserAgentParser.Benchmarks/UserAgentBenchmarks.cs b/perf/MyCSharp.HttpUserAgentParser.Benchmarks/UserAgentBenchmarks.cs new file mode 100644 index 0000000..76a77bd --- /dev/null +++ b/perf/MyCSharp.HttpUserAgentParser.Benchmarks/UserAgentBenchmarks.cs @@ -0,0 +1,70 @@ +// Copyright © myCSharp 2020-2021, all rights reserved + +using System.Collections.Generic; +using System.Linq; +using BenchmarkDotNet.Attributes; +using MyCSharp.HttpUserAgentParser.Benchmarks.ExternalCode; +using UAParser; + +namespace MyCSharp.HttpUserAgentParser.Benchmarks +{ + [MemoryDiagnoser] +#if OS_WIN + [EtwProfiler] // needs admin-rights +#endif + public class UserAgentBenchmarks + { + private Parser _uaParser; + + private string[] _testUserAgentMix; + + private static IEnumerable GetTestUserAgents() + { + yield return + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36"; + yield return "APIs-Google (+https://developers.google.com/webmasters/APIs-Google.html)"; + yield return "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:88.0) Gecko/20100101 Firefox/88.0"; + yield return "yeah I'm unknown user agent, just to bring some fun to the mix"; + } + + [GlobalSetup] + public void Setup() + { + _uaParser = UAParser.Parser.GetDefault(new ParserOptions()); + _testUserAgentMix = GetTestUserAgents().ToArray(); + } + + [Benchmark(Description = "UA Parser")] + public void UAParserTest() + { + string[] testUserAgentMix = _testUserAgentMix; + + for (int i = 0; i < testUserAgentMix.Length; ++i) + { + _uaParser.Parse(testUserAgentMix[i]); + } + } + + [Benchmark(Description = "UserAgentService")] + public void UserAgentServiceTest() + { + string[] testUserAgentMix = _testUserAgentMix; + + for (int i = 0; i < testUserAgentMix.Length; ++i) + { + new UserAgentServiceUserAgent(testUserAgentMix[i]); + } + } + + [Benchmark(Description = "HttpUserAgentParser")] + public void HttpUserAgentParserTest() + { + string[] testUserAgentMix = _testUserAgentMix; + + for (int i = 0; i < testUserAgentMix.Length; ++i) + { + HttpUserAgentParser.Parse(testUserAgentMix[i]); + } + } + } +} diff --git a/src/MyCSharp.HttpUserAgentParser.AspNetCore/DependencyInjection/HttpUserAgentParserDependencyInjectionOptionsExtensions.cs b/src/MyCSharp.HttpUserAgentParser.AspNetCore/DependencyInjection/HttpUserAgentParserDependencyInjectionOptionsExtensions.cs new file mode 100644 index 0000000..7676eb8 --- /dev/null +++ b/src/MyCSharp.HttpUserAgentParser.AspNetCore/DependencyInjection/HttpUserAgentParserDependencyInjectionOptionsExtensions.cs @@ -0,0 +1,22 @@ +// Copyright © myCSharp 2020-2021, all rights reserved + +using Microsoft.Extensions.DependencyInjection; +using MyCSharp.HttpUserAgentParser.DependencyInjection; +using MyCSharp.HttpUserAgentParser.Providers; + +namespace MyCSharp.HttpUserAgentParser.AspNetCore.DependencyInjection +{ + public static class HttpUserAgentParserDependencyInjectionOptionsExtensions + { + /// + /// Registers as . + /// Requires a registered + /// + public static HttpUserAgentParserDependencyInjectionOptions AddHttpUserAgentParserAccessor( + this HttpUserAgentParserDependencyInjectionOptions options) + { + options.Services.AddSingleton(); + return options; + } + } +} \ No newline at end of file diff --git a/src/MyCSharp.HttpUserAgentParser.AspNetCore/HttpUserAgentParserAccessor.cs b/src/MyCSharp.HttpUserAgentParser.AspNetCore/HttpUserAgentParserAccessor.cs new file mode 100644 index 0000000..261a924 --- /dev/null +++ b/src/MyCSharp.HttpUserAgentParser.AspNetCore/HttpUserAgentParserAccessor.cs @@ -0,0 +1,31 @@ +// Copyright © myCSharp 2020-2021, all rights reserved + +using Microsoft.AspNetCore.Http; +using MyCSharp.HttpUserAgentParser.Providers; + +namespace MyCSharp.HttpUserAgentParser.AspNetCore +{ + public interface IHttpUserAgentParserAccessor + { + string HttpContextUserAgent { get; } + HttpUserAgentInformation Get(); + } + + public class HttpUserAgentParserAccessor : IHttpUserAgentParserAccessor + { + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly IHttpUserAgentParserProvider _httpUserAgentParser; + + public HttpUserAgentParserAccessor(IHttpContextAccessor httpContextAccessor, IHttpUserAgentParserProvider httpUserAgentParser) + { + _httpContextAccessor = httpContextAccessor; + _httpUserAgentParser = httpUserAgentParser; + } + + public string HttpContextUserAgent => + _httpContextAccessor.HttpContext?.Request?.Headers["User-Agent"].ToString()!; + + public HttpUserAgentInformation Get() + => _httpUserAgentParser.Parse(this.HttpContextUserAgent); + } +} diff --git a/src/MyCSharp.HttpUserAgentParser.AspNetCore/MyCSharp.HttpUserAgentParser.AspNetCore.csproj b/src/MyCSharp.HttpUserAgentParser.AspNetCore/MyCSharp.HttpUserAgentParser.AspNetCore.csproj new file mode 100644 index 0000000..241883a --- /dev/null +++ b/src/MyCSharp.HttpUserAgentParser.AspNetCore/MyCSharp.HttpUserAgentParser.AspNetCore.csproj @@ -0,0 +1,17 @@ + + + + HTTP User Agent Parser Extensions for ASP.NET Core + HTTP User Agent Parser Extensions for ASP.NET Core + net5.0 + + + + + + + + + + + \ No newline at end of file diff --git a/src/MyCSharp.HttpUserAgentParser.MemoryCache/DependencyInjection/HttpUserAgentParserMemoryCacheServiceCollectionExtensions.cs b/src/MyCSharp.HttpUserAgentParser.MemoryCache/DependencyInjection/HttpUserAgentParserMemoryCacheServiceCollectionExtensions.cs new file mode 100644 index 0000000..21b7890 --- /dev/null +++ b/src/MyCSharp.HttpUserAgentParser.MemoryCache/DependencyInjection/HttpUserAgentParserMemoryCacheServiceCollectionExtensions.cs @@ -0,0 +1,28 @@ +// Copyright © myCSharp 2020-2021, all rights reserved + +using System; +using Microsoft.Extensions.DependencyInjection; +using MyCSharp.HttpUserAgentParser.DependencyInjection; +using MyCSharp.HttpUserAgentParser.Providers; + +namespace MyCSharp.HttpUserAgentParser.MemoryCache.DependencyInjection +{ + public static class HttpUserAgentParserMemoryCacheServiceCollectionExtensions + { + /// + /// Registers as singleton to + /// + public static HttpUserAgentParserDependencyInjectionOptions AddHttpUserAgentMemoryCachedParser( + this IServiceCollection services, Action? options = null) + { + HttpUserAgentParserMemoryCachedProviderOptions providerOptions = new(); + options?.Invoke(providerOptions); + + // register options + services.AddSingleton(providerOptions); + + // register cache provider + return services.AddHttpUserAgentParser(); + } + } +} diff --git a/src/MyCSharp.HttpUserAgentParser.MemoryCache/HttpUserAgentParserMemoryCachedProvider.cs b/src/MyCSharp.HttpUserAgentParser.MemoryCache/HttpUserAgentParserMemoryCachedProvider.cs new file mode 100644 index 0000000..f618c5f --- /dev/null +++ b/src/MyCSharp.HttpUserAgentParser.MemoryCache/HttpUserAgentParserMemoryCachedProvider.cs @@ -0,0 +1,69 @@ +// Copyright © myCSharp 2020-2021, all rights reserved + +using System; +using Microsoft.Extensions.Caching.Memory; +using MyCSharp.HttpUserAgentParser.Providers; + +namespace MyCSharp.HttpUserAgentParser.MemoryCache +{ + public class HttpUserAgentParserMemoryCachedProvider : IHttpUserAgentParserProvider + { + private readonly IMemoryCache _memoryCache; + private readonly HttpUserAgentParserMemoryCachedProviderOptions _options; + + public HttpUserAgentParserMemoryCachedProvider(IMemoryCache memoryCache, HttpUserAgentParserMemoryCachedProviderOptions options) + { + _memoryCache = memoryCache; + _options = options; + } + + public HttpUserAgentInformation Parse(string userAgent) + { + CacheKey key = this.GetKey(userAgent); + + return _memoryCache.GetOrCreate(key, static entry => + { + CacheKey key = (entry.Key as CacheKey)!; + entry.SlidingExpiration = key.Options.CacheEntryOptions.SlidingExpiration; + entry.SetSize(1); + + return HttpUserAgentParser.Parse(key.UserAgent); + }); + } + + [ThreadStatic] + private static CacheKey? s_tKey; + + private CacheKey GetKey(string userAgent) + { + CacheKey key = s_tKey ??= new CacheKey(); + + key.UserAgent = userAgent; + key.Options = _options; + + return key; + } + + private class CacheKey : IEquatable // required for IMemoryCache + { + public string UserAgent { get; set; } = null!; + + public HttpUserAgentParserMemoryCachedProviderOptions Options { get; set; } = null!; + + public bool Equals(CacheKey? other) + { + if (ReferenceEquals(this, other)) return true; + if (other is null) return false; + + return this.UserAgent == other.UserAgent && this.Options == other.Options; + } + + public override bool Equals(object? obj) + { + return this.Equals(obj as CacheKey); + } + + public override int GetHashCode() => HashCode.Combine(this.UserAgent, this.Options); + } + } +} diff --git a/src/MyCSharp.HttpUserAgentParser.MemoryCache/HttpUserAgentParserMemoryCachedProviderOptions.cs b/src/MyCSharp.HttpUserAgentParser.MemoryCache/HttpUserAgentParserMemoryCachedProviderOptions.cs new file mode 100644 index 0000000..8ef3fef --- /dev/null +++ b/src/MyCSharp.HttpUserAgentParser.MemoryCache/HttpUserAgentParserMemoryCachedProviderOptions.cs @@ -0,0 +1,40 @@ +// Copyright © myCSharp 2020-2021, all rights reserved + +using System; +using Microsoft.Extensions.Caching.Memory; + +namespace MyCSharp.HttpUserAgentParser.MemoryCache +{ + /// + /// Provider options for + /// + /// Default of is 256. + /// Default of is 1 day + /// + /// + public class HttpUserAgentParserMemoryCachedProviderOptions + { + public MemoryCacheOptions CacheOptions { get; } + public MemoryCacheEntryOptions CacheEntryOptions { get; } + + public HttpUserAgentParserMemoryCachedProviderOptions(MemoryCacheOptions cacheOptions) + : this(cacheOptions, null) { } + + public HttpUserAgentParserMemoryCachedProviderOptions(MemoryCacheEntryOptions cacheEntryOptions) + : this(null, cacheEntryOptions) { } + + public HttpUserAgentParserMemoryCachedProviderOptions(MemoryCacheOptions? cacheOptions = null, MemoryCacheEntryOptions? cacheEntryOptions = null) + { + this.CacheEntryOptions = cacheEntryOptions ?? new MemoryCacheEntryOptions + { + // defaults + SlidingExpiration = TimeSpan.FromDays(1) + }; + this.CacheOptions = cacheOptions ?? new MemoryCacheOptions + { + // defaults + SizeLimit = 256 + }; + } + } +} diff --git a/src/MyCSharp.HttpUserAgentParser.MemoryCache/MyCSharp.HttpUserAgentParser.MemoryCache.csproj b/src/MyCSharp.HttpUserAgentParser.MemoryCache/MyCSharp.HttpUserAgentParser.MemoryCache.csproj new file mode 100644 index 0000000..161ab49 --- /dev/null +++ b/src/MyCSharp.HttpUserAgentParser.MemoryCache/MyCSharp.HttpUserAgentParser.MemoryCache.csproj @@ -0,0 +1,18 @@ + + + + HTTP User Agent Parser Extensions for IMemoryCache + HTTP User Agent Parser Extensions for IMemoryCache + netstandard2.1 + + + + + + + + + + + + \ No newline at end of file diff --git a/src/MyCSharp.HttpUserAgentParser/DependencyInjection/HttpUserAgentParserDependencyInjectionOptions.cs b/src/MyCSharp.HttpUserAgentParser/DependencyInjection/HttpUserAgentParserDependencyInjectionOptions.cs new file mode 100644 index 0000000..c2d6715 --- /dev/null +++ b/src/MyCSharp.HttpUserAgentParser/DependencyInjection/HttpUserAgentParserDependencyInjectionOptions.cs @@ -0,0 +1,16 @@ +// Copyright © myCSharp 2020-2021, all rights reserved + +using Microsoft.Extensions.DependencyInjection; + +namespace MyCSharp.HttpUserAgentParser.DependencyInjection +{ + public class HttpUserAgentParserDependencyInjectionOptions + { + public IServiceCollection Services { get; } + + public HttpUserAgentParserDependencyInjectionOptions(IServiceCollection services) + { + Services = services; + } + } +} \ No newline at end of file diff --git a/src/MyCSharp.HttpUserAgentParser/DependencyInjection/HttpUserAgentParserServiceCollectionExtensions.cs b/src/MyCSharp.HttpUserAgentParser/DependencyInjection/HttpUserAgentParserServiceCollectionExtensions.cs new file mode 100644 index 0000000..6406501 --- /dev/null +++ b/src/MyCSharp.HttpUserAgentParser/DependencyInjection/HttpUserAgentParserServiceCollectionExtensions.cs @@ -0,0 +1,43 @@ +// Copyright © myCSharp 2020-2021, all rights reserved + +using Microsoft.Extensions.DependencyInjection; +using MyCSharp.HttpUserAgentParser.Providers; + +namespace MyCSharp.HttpUserAgentParser.DependencyInjection +{ + public static class HttpUserAgentParserServiceCollectionExtensions + { + /// + /// Registers as singleton to + /// + public static HttpUserAgentParserDependencyInjectionOptions AddHttpUserAgentParser( + this IServiceCollection services) + { + return AddHttpUserAgentParser(services); + } + + /// + /// Registers as singleton to + /// + public static HttpUserAgentParserDependencyInjectionOptions AddHttpUserAgentCachedParser( + this IServiceCollection services) + { + return AddHttpUserAgentParser(services); + } + + /// + /// Registers as singleton to + /// + public static HttpUserAgentParserDependencyInjectionOptions AddHttpUserAgentParser( + this IServiceCollection services) where TProvider : class, IHttpUserAgentParserProvider + { + // create options + HttpUserAgentParserDependencyInjectionOptions options = new(services); + + // add provider + services.AddSingleton(); + + return options; + } + } +} diff --git a/src/MyCSharp.HttpUserAgentParser/HttpUserAgentInformation.cs b/src/MyCSharp.HttpUserAgentParser/HttpUserAgentInformation.cs new file mode 100644 index 0000000..cfb1c3e --- /dev/null +++ b/src/MyCSharp.HttpUserAgentParser/HttpUserAgentInformation.cs @@ -0,0 +1,40 @@ +// Copyright © myCSharp 2020-2021, all rights reserved + +namespace MyCSharp.HttpUserAgentParser +{ + public readonly struct HttpUserAgentInformation + { + public string UserAgent { get; } + public HttpUserAgentType Type { get; } + + public HttpUserAgentPlatformInformation? Platform { get; } + public string? Name { get; } + public string? Version { get; } + public string? MobileDeviceType { get; } + + private HttpUserAgentInformation(string userAgent, HttpUserAgentPlatformInformation? platform, HttpUserAgentType type, string? name, string? version, string? deviceName) + { + UserAgent = userAgent; + Type = type; + Name = name; + Platform = platform; + Version = version; + MobileDeviceType = deviceName; + } + + // parse + + public static HttpUserAgentInformation Parse(string userAgent) => HttpUserAgentParser.Parse(userAgent); + + // create factories + + public static HttpUserAgentInformation CreateForRobot(string userAgent, string robotName) + => new(userAgent, null, HttpUserAgentType.Robot, robotName, null, null); + + public static HttpUserAgentInformation CreateForBrowser(string userAgent, HttpUserAgentPlatformInformation? platform, string? browserName, string? browserVersion, string? deviceName) + => new(userAgent, platform, HttpUserAgentType.Browser, browserName, browserVersion, deviceName); + + public static HttpUserAgentInformation CreateForUnknown(string userAgent, HttpUserAgentPlatformInformation? platform, string? deviceName) + => new(userAgent, platform, HttpUserAgentType.Unknown, null, null, deviceName); + } +} diff --git a/src/MyCSharp.HttpUserAgentParser/HttpUserAgentInformationExtensions.cs b/src/MyCSharp.HttpUserAgentParser/HttpUserAgentInformationExtensions.cs new file mode 100644 index 0000000..4e3f083 --- /dev/null +++ b/src/MyCSharp.HttpUserAgentParser/HttpUserAgentInformationExtensions.cs @@ -0,0 +1,12 @@ +// Copyright © myCSharp 2020-2021, all rights reserved + +namespace MyCSharp.HttpUserAgentParser +{ + public static class HttpUserAgentInformationExtensions + { + public static bool IsType(this in HttpUserAgentInformation userAgent, HttpUserAgentType type) => userAgent.Type == type; + public static bool IsRobot(this in HttpUserAgentInformation userAgent) => IsType(userAgent, HttpUserAgentType.Robot); + public static bool IsBrowser(this in HttpUserAgentInformation userAgent) => IsType(userAgent, HttpUserAgentType.Browser); + public static bool IsMobile(this in HttpUserAgentInformation userAgent) => userAgent.MobileDeviceType is not null; + } +} diff --git a/src/MyCSharp.HttpUserAgentParser/HttpUserAgentParser.cs b/src/MyCSharp.HttpUserAgentParser/HttpUserAgentParser.cs new file mode 100644 index 0000000..dfb33aa --- /dev/null +++ b/src/MyCSharp.HttpUserAgentParser/HttpUserAgentParser.cs @@ -0,0 +1,112 @@ +// Copyright © myCSharp 2020-2021, all rights reserved + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Text.RegularExpressions; + +namespace MyCSharp.HttpUserAgentParser +{ + public static class HttpUserAgentParser + { + public static HttpUserAgentInformation Parse(string userAgent) + { + // prepare + userAgent = Cleanup(userAgent); + + // analyze + if (TryGetRobot(userAgent, out string? robotName)) + { + return HttpUserAgentInformation.CreateForRobot(userAgent, robotName); + } + + HttpUserAgentPlatformInformation? platform = GetPlatform(userAgent); + string? mobileDeviceType = GetMobileDevice(userAgent); + + if (TryGetBrowser(userAgent, out (string Name, string? Version)? browser)) + { + return HttpUserAgentInformation.CreateForBrowser(userAgent, platform, browser?.Name, browser?.Version, mobileDeviceType); + } + + return HttpUserAgentInformation.CreateForUnknown(userAgent, platform, mobileDeviceType); + } + + public static string Cleanup(string userAgent) => userAgent.Trim(); + + public static HttpUserAgentPlatformInformation? GetPlatform(string userAgent) + { + foreach (HttpUserAgentPlatformInformation item in HttpUserAgentStatics.Platforms) + { + if (item.Regex.IsMatch(userAgent)) + { + return item; + } + } + + return null; + } + + public static bool TryGetPlatform(string userAgent, [NotNullWhen(true)] out HttpUserAgentPlatformInformation? platform) + { + platform = GetPlatform(userAgent); + return platform is not null; + } + + public static (string Name, string? Version)? GetBrowser(string userAgent) + { + foreach ((Regex key, string? value) in HttpUserAgentStatics.Browsers) + { + Match match = key.Match(userAgent); + if (match.Success) + { + return (value, match.Groups[1].Value); + } + } + + return null; + } + + public static bool TryGetBrowser(string userAgent, [NotNullWhen(true)] out (string Name, string? Version)? browser) + { + browser = GetBrowser(userAgent); + return browser is not null; + } + + public static string? GetRobot(string userAgent) + { + foreach ((string key, string value) in HttpUserAgentStatics.Robots) + { + if (userAgent.Contains(key, StringComparison.OrdinalIgnoreCase)) + { + return value; + } + } + + return null; + } + + public static bool TryGetRobot(string userAgent, [NotNullWhen(true)] out string? robotName) + { + robotName = GetRobot(userAgent); + return robotName is not null; + } + + public static string? GetMobileDevice(string userAgent) + { + foreach ((string key, string value) in HttpUserAgentStatics.Mobiles) + { + if (userAgent.Contains(key, StringComparison.OrdinalIgnoreCase)) + { + return value; + } + } + + return null; + } + + public static bool TryGetMobileDevice(string userAgent, [NotNullWhen(true)] out string? device) + { + device = GetMobileDevice(userAgent); + return device is not null; + } + } +} diff --git a/src/MyCSharp.HttpUserAgentParser/HttpUserAgentPlatformInformation.cs b/src/MyCSharp.HttpUserAgentParser/HttpUserAgentPlatformInformation.cs new file mode 100644 index 0000000..85be234 --- /dev/null +++ b/src/MyCSharp.HttpUserAgentParser/HttpUserAgentPlatformInformation.cs @@ -0,0 +1,20 @@ +// Copyright © myCSharp 2020-2021, all rights reserved + +using System.Text.RegularExpressions; + +namespace MyCSharp.HttpUserAgentParser +{ + public readonly struct HttpUserAgentPlatformInformation + { + public Regex Regex { get; } + public string Name { get; } + public HttpUserAgentPlatformType PlatformType { get; } + + public HttpUserAgentPlatformInformation(Regex regex, string name, HttpUserAgentPlatformType platformType) + { + Regex = regex; + Name = name; + PlatformType = platformType; + } + } +} \ No newline at end of file diff --git a/src/MyCSharp.HttpUserAgentParser/HttpUserAgentPlatformType.cs b/src/MyCSharp.HttpUserAgentParser/HttpUserAgentPlatformType.cs new file mode 100644 index 0000000..b491651 --- /dev/null +++ b/src/MyCSharp.HttpUserAgentParser/HttpUserAgentPlatformType.cs @@ -0,0 +1,18 @@ +// Copyright © myCSharp 2020-2021, all rights reserved + +namespace MyCSharp.HttpUserAgentParser +{ + public enum HttpUserAgentPlatformType : byte + { + Unknown = 0, + Generic, + Windows, + Linux, + Unix, + IOS, + MacOS, + BlackBerry, + Android, + Symbian + } +} diff --git a/src/MyCSharp.HttpUserAgentParser/HttpUserAgentStatics.cs b/src/MyCSharp.HttpUserAgentParser/HttpUserAgentStatics.cs new file mode 100644 index 0000000..cda789c --- /dev/null +++ b/src/MyCSharp.HttpUserAgentParser/HttpUserAgentStatics.cs @@ -0,0 +1,236 @@ +// Copyright © myCSharp 2020-2021, all rights reserved + +using System.Collections.Generic; +using System.Text.RegularExpressions; + +namespace MyCSharp.HttpUserAgentParser +{ + public static class HttpUserAgentStatics + { + + private const RegexOptions DefaultPlatformsRegexFlags = RegexOptions.IgnoreCase | RegexOptions.Compiled; + private static Regex CreateDefaultPlatformRegex(string key) => new(Regex.Escape($"{key}"), DefaultPlatformsRegexFlags); + public static readonly HashSet Platforms = new() + { + new(CreateDefaultPlatformRegex("windows nt 10.0"), "Windows 10", HttpUserAgentPlatformType.Windows), + new(CreateDefaultPlatformRegex("windows nt 6.3"), "Windows 8.1", HttpUserAgentPlatformType.Windows), + new(CreateDefaultPlatformRegex("windows nt 6.2"), "Windows 8", HttpUserAgentPlatformType.Windows), + new(CreateDefaultPlatformRegex("windows nt 6.1"), "Windows 7", HttpUserAgentPlatformType.Windows), + new(CreateDefaultPlatformRegex("windows nt 6.0"), "Windows Vista", HttpUserAgentPlatformType.Windows), + new(CreateDefaultPlatformRegex("windows nt 5.2"), "Windows 2003", HttpUserAgentPlatformType.Windows), + new(CreateDefaultPlatformRegex("windows nt 5.1"), "Windows XP", HttpUserAgentPlatformType.Windows), + new(CreateDefaultPlatformRegex("windows nt 5.0"), "Windows 2000", HttpUserAgentPlatformType.Windows), + new(CreateDefaultPlatformRegex("windows nt 4.0"), "Windows NT 4.0", HttpUserAgentPlatformType.Windows), + new(CreateDefaultPlatformRegex("winnt4.0"), "Windows NT 4.0", HttpUserAgentPlatformType.Windows), + new(CreateDefaultPlatformRegex("winnt 4.0"), "Windows NT", HttpUserAgentPlatformType.Windows), + new(CreateDefaultPlatformRegex("winnt"), "Windows NT", HttpUserAgentPlatformType.Windows), + new(CreateDefaultPlatformRegex("windows 98"), "Windows 98", HttpUserAgentPlatformType.Windows), + new(CreateDefaultPlatformRegex("win98"), "Windows 98", HttpUserAgentPlatformType.Windows), + new(CreateDefaultPlatformRegex("windows 95"), "Windows 95", HttpUserAgentPlatformType.Windows), + new(CreateDefaultPlatformRegex("win95"), "Windows 95", HttpUserAgentPlatformType.Windows), + new(CreateDefaultPlatformRegex("windows phone"), "Windows Phone", HttpUserAgentPlatformType.Windows), + new(CreateDefaultPlatformRegex("windows"), "Unknown Windows OS", HttpUserAgentPlatformType.Windows), + new(CreateDefaultPlatformRegex("android"), "Android", HttpUserAgentPlatformType.Android), + new(CreateDefaultPlatformRegex("blackberry"), "BlackBerry", HttpUserAgentPlatformType.BlackBerry), + new(CreateDefaultPlatformRegex("iphone"), "iOS", HttpUserAgentPlatformType.IOS), + new(CreateDefaultPlatformRegex("ipad"), "iOS", HttpUserAgentPlatformType.IOS), + new(CreateDefaultPlatformRegex("ipod"), "iOS", HttpUserAgentPlatformType.IOS), + new(CreateDefaultPlatformRegex("os x"), "Mac OS X", HttpUserAgentPlatformType.MacOS), + new(CreateDefaultPlatformRegex("ppc mac"), "Power PC Mac", HttpUserAgentPlatformType.MacOS), + new(CreateDefaultPlatformRegex("freebsd"), "FreeBSD", HttpUserAgentPlatformType.Linux), + new(CreateDefaultPlatformRegex("ppc"), "Macintosh", HttpUserAgentPlatformType.Linux), + new(CreateDefaultPlatformRegex("linux"), "Linux", HttpUserAgentPlatformType.Linux), + new(CreateDefaultPlatformRegex("debian"), "Debian", HttpUserAgentPlatformType.Linux), + new(CreateDefaultPlatformRegex("sunos"), "Sun Solaris", HttpUserAgentPlatformType.Generic), + new(CreateDefaultPlatformRegex("beos"), "BeOS", HttpUserAgentPlatformType.Generic), + new(CreateDefaultPlatformRegex("apachebench"), "ApacheBench", HttpUserAgentPlatformType.Generic), + new(CreateDefaultPlatformRegex("aix"), "AIX", HttpUserAgentPlatformType.Generic), + new(CreateDefaultPlatformRegex("irix"), "Irix", HttpUserAgentPlatformType.Generic), + new(CreateDefaultPlatformRegex("osf"), "DEC OSF", HttpUserAgentPlatformType.Generic), + new(CreateDefaultPlatformRegex("hp-ux"), "HP-UX", HttpUserAgentPlatformType.Windows), + new(CreateDefaultPlatformRegex("netbsd"), "NetBSD", HttpUserAgentPlatformType.Generic), + new(CreateDefaultPlatformRegex("bsdi"), "BSDi", HttpUserAgentPlatformType.Generic), + new(CreateDefaultPlatformRegex("openbsd"), "OpenBSD", HttpUserAgentPlatformType.Unix), + new(CreateDefaultPlatformRegex("gnu"), "GNU/Linux", HttpUserAgentPlatformType.Linux), + new(CreateDefaultPlatformRegex("unix"), "Unknown Unix OS", HttpUserAgentPlatformType.Unix), + new(CreateDefaultPlatformRegex("symbian"), "Symbian OS", HttpUserAgentPlatformType.Symbian), + }; + + private const RegexOptions DefaultBrowserRegexFlags = RegexOptions.IgnoreCase | RegexOptions.Compiled; + private static Regex CreateDefaultBrowserRegex(string key) => new($@"{key}.*?([0-9\.]+)", DefaultBrowserRegexFlags); + public static Dictionary Browsers = new() + { + { CreateDefaultBrowserRegex("OPR"), "Opera" }, + { CreateDefaultBrowserRegex("Flock"), "Flock" }, + { CreateDefaultBrowserRegex("Edge"), "Edge" }, + { CreateDefaultBrowserRegex("EdgA"), "Edge" }, + { CreateDefaultBrowserRegex("Edg"), "Edge" }, + { CreateDefaultBrowserRegex("Vivaldi"), "Vivaldi" }, + { CreateDefaultBrowserRegex("Brave Chrome"), "Brave" }, + { CreateDefaultBrowserRegex("Chrome"), "Chrome" }, + { CreateDefaultBrowserRegex("CriOS"), "Chrome" }, + { CreateDefaultBrowserRegex("Opera.*?Version"), "Opera" }, + { CreateDefaultBrowserRegex("Opera"), "Opera" }, + { CreateDefaultBrowserRegex("MSIE"), "Internet Explorer" }, + { CreateDefaultBrowserRegex("Internet Explorer"), "Internet Explorer" }, + { CreateDefaultBrowserRegex("Trident.* rv"), "Internet Explorer" }, + { CreateDefaultBrowserRegex("Shiira"), "Shiira" }, + { CreateDefaultBrowserRegex("Firefox"), "Firefox" }, + { CreateDefaultBrowserRegex("FxiOS"), "Firefox" }, + { CreateDefaultBrowserRegex("Chimera"), "Chimera" }, + { CreateDefaultBrowserRegex("Phoenix"), "Phoenix" }, + { CreateDefaultBrowserRegex("Firebird"), "Firebird" }, + { CreateDefaultBrowserRegex("Camino"), "Camino" }, + { CreateDefaultBrowserRegex("Netscape"), "Netscape" }, + { CreateDefaultBrowserRegex("OmniWeb"), "OmniWeb" }, + { CreateDefaultBrowserRegex("Safari"), "Safari" }, + { CreateDefaultBrowserRegex("Mozilla"), "Mozilla" }, + { CreateDefaultBrowserRegex("Konqueror"), "Konqueror" }, + { CreateDefaultBrowserRegex("icab"), "iCab" }, + { CreateDefaultBrowserRegex("Lynx"), "Lynx" }, + { CreateDefaultBrowserRegex("Links"), "Links" }, + { CreateDefaultBrowserRegex("hotjava"), "HotJava" }, + { CreateDefaultBrowserRegex("amaya"), "Amaya" }, + { CreateDefaultBrowserRegex("IBrowse"), "IBrowse" }, + { CreateDefaultBrowserRegex("Maxthon"), "Maxthon" }, + { CreateDefaultBrowserRegex("ipod touch"), "Apple iPod" }, + { CreateDefaultBrowserRegex("Ubuntu"), "Ubuntu Web Browser" }, + }; + + public static readonly Dictionary Mobiles = new() + { + // Legacy + { "mobileexplorer", "Mobile Explorer" }, + { "palmsource", "Palm" }, + { "palmscape", "Palmscape" }, + // Phones and Manufacturers + { "motorola", "Motorola" }, + { "nokia", "Nokia" }, + { "palm", "Palm" }, + { "ipad", "Apple iPad" }, + { "ipod", "Apple iPod" }, + { "iphone", "Apple iPhone" }, + { "sony", "Sony Ericsson" }, + { "ericsson", "Sony Ericsson" }, + { "blackberry", "BlackBerry" }, + { "cocoon", "O2 Cocoon" }, + { "blazer", "Treo" }, + { "lg", "LG" }, + { "amoi", "Amoi" }, + { "xda", "XDA" }, + { "mda", "MDA" }, + { "vario", "Vario" }, + { "htc", "HTC" }, + { "samsung", "Samsung" }, + { "sharp", "Sharp" }, + { "sie-", "Siemens" }, + { "alcatel", "Alcatel" }, + { "benq", "BenQ" }, + { "ipaq", "HP iPaq" }, + { "mot-", "Motorola" }, + { "playstation portable", "PlayStation Portable" }, + { "playstation 3", "PlayStation 3" }, + { "playstation vita", "PlayStation Vita" }, + { "hiptop", "Danger Hiptop" }, + { "nec-", "NEC" }, + { "panasonic", "Panasonic" }, + { "philips", "Philips" }, + { "sagem", "Sagem" }, + { "sanyo", "Sanyo" }, + { "spv", "SPV" }, + { "zte", "ZTE" }, + { "sendo", "Sendo" }, + { "nintendo dsi", "Nintendo DSi" }, + { "nintendo ds", "Nintendo DS" }, + { "nintendo 3ds", "Nintendo 3DS" }, + { "wii", "Nintendo Wii" }, + { "open web", "Open Web" }, + { "openweb", "OpenWeb" }, + // Operating Systems + { "android", "Android" }, + { "symbian", "Symbian" }, + { "SymbianOS", "SymbianOS" }, + { "elaine", "Palm" }, + { "series60", "Symbian S60" }, + { "windows ce", "Windows CE" }, + // Browsers + { "obigo", "Obigo" }, + { "netfront", "Netfront Browser" }, + { "openwave", "Openwave Browser" }, + { "mobilexplorer", "Mobile Explorer" }, + { "operamini", "Opera Mini" }, + { "opera mini", "Opera Mini" }, + { "opera mobi", "Opera Mobile" }, + { "fennec", "Firefox Mobile" }, + // Other + { "digital paths", "Digital Paths" }, + { "avantgo", "AvantGo" }, + { "xiino", "Xiino" }, + { "novarra", "Novarra Transcoder" }, + { "vodafone", "Vodafone" }, + { "docomo", "NTT DoCoMo" }, + { "o2", "O2" }, + // Fallback + { "mobile", "Generic Mobile" }, + { "wireless", "Generic Mobile" }, + { "j2me", "Generic Mobile" }, + { "midp", "Generic Mobile" }, + { "cldc", "Generic Mobile" }, + { "up.link", "Generic Mobile" }, + { "up.browser", "Generic Mobile" }, + { "smartphone", "Generic Mobile" }, + { "cellphone", "Generic Mobile" }, + }; + + public static readonly (string Key, string Value)[] Robots = + { + ( "googlebot", "Googlebot" ), + ( "googleweblight", "Google Web Light" ), + ( "PetalBot", "PetalBot"), + ( "DuplexWeb-Google", "DuplexWeb-Google"), + ( "Storebot-Google", "Storebot-Google"), + ( "msnbot", "MSNBot"), + ( "baiduspider", "Baiduspider"), + ( "Google Favicon", "Google Favicon"), + ( "Jobboerse", "Jobboerse"), + ( "bingbot", "BingBot"), + ( "BingPreview", "Bing Preview"), + ( "slurp", "Slurp"), + ( "yahoo", "Yahoo"), + ( "ask jeeves", "Ask Jeeves"), + ( "fastcrawler", "FastCrawler"), + ( "infoseek", "InfoSeek Robot 1.0"), + ( "lycos", "Lycos"), + ( "YandexBot", "YandexBot"), + ( "YandexImages", "YandexImages"), + ( "mediapartners-google", "Mediapartners Google"), + ( "apis-google", "APIs Google"), + ( "CRAZYWEBCRAWLER", "Crazy Webcrawler"), + ( "AdsBot-Google-Mobile", "AdsBot Google Mobile"), + ( "adsbot-google", "AdsBot Google"), + ( "feedfetcher-google", "FeedFetcher-Google"), + ( "google-read-aloud", "Google-Read-Aloud"), + ( "curious george", "Curious George"), + ( "ia_archiver", "Alexa Crawler"), + ( "MJ12bot", "Majestic"), + ( "Uptimebot", "Uptimebot"), + ( "CheckMarkNetwork", "CheckMark"), + ( "facebookexternalhit", "Facebook"), + ( "adscanner", "AdScanner"), + ( "AhrefsBot", "Ahrefs"), + ( "BLEXBot", "BLEXBot"), + ( "DotBot", "OpenSite"), + ( "Mail.RU_Bot", "Mail.ru"), + ( "MegaIndex", "MegaIndex"), + ( "SemrushBot", "SEMRush"), + ( "SEOkicks", "SEOkicks"), + ( "seoscanners.net", "SEO Scanners"), + ( "Sistrix", "Sistrix" ) + }; + + public static readonly Dictionary Tools = new() + { + { "curl", "curl" } + }; + } +} diff --git a/src/MyCSharp.HttpUserAgentParser/HttpUserAgentType.cs b/src/MyCSharp.HttpUserAgentParser/HttpUserAgentType.cs new file mode 100644 index 0000000..e75c7a4 --- /dev/null +++ b/src/MyCSharp.HttpUserAgentParser/HttpUserAgentType.cs @@ -0,0 +1,11 @@ +// Copyright © myCSharp 2020-2021, all rights reserved + +namespace MyCSharp.HttpUserAgentParser +{ + public enum HttpUserAgentType : byte + { + Unknown, + Browser, + Robot, + } +} diff --git a/src/MyCSharp.HttpUserAgentParser/MyCSharp.HttpUserAgentParser.csproj b/src/MyCSharp.HttpUserAgentParser/MyCSharp.HttpUserAgentParser.csproj new file mode 100644 index 0000000..dd386d8 --- /dev/null +++ b/src/MyCSharp.HttpUserAgentParser/MyCSharp.HttpUserAgentParser.csproj @@ -0,0 +1,13 @@ + + + + HTTP User Agent Parser + Parses user agents for Browser, Platform and Bots. + netstandard2.1 + + + + + + + \ No newline at end of file diff --git a/src/MyCSharp.HttpUserAgentParser/Providers/HttpUserAgentParserCachedProvider.cs b/src/MyCSharp.HttpUserAgentParser/Providers/HttpUserAgentParserCachedProvider.cs new file mode 100644 index 0000000..eaa86cd --- /dev/null +++ b/src/MyCSharp.HttpUserAgentParser/Providers/HttpUserAgentParserCachedProvider.cs @@ -0,0 +1,17 @@ +// Copyright © myCSharp 2020-2021, all rights reserved + +using System.Collections.Concurrent; + +namespace MyCSharp.HttpUserAgentParser.Providers +{ + public class HttpUserAgentParserCachedProvider : IHttpUserAgentParserProvider + { + private readonly ConcurrentDictionary _cache = new(); + + public HttpUserAgentInformation Parse(string userAgent) + => _cache.GetOrAdd(userAgent, static ua => HttpUserAgentParser.Parse(ua)); + + public int CacheEntryCount => _cache.Count; + public bool HasCacheEntry(string userAgent) => _cache.ContainsKey(userAgent); + } +} diff --git a/src/MyCSharp.HttpUserAgentParser/Providers/HttpUserAgentParserDefaultProvider.cs b/src/MyCSharp.HttpUserAgentParser/Providers/HttpUserAgentParserDefaultProvider.cs new file mode 100644 index 0000000..9cdb2db --- /dev/null +++ b/src/MyCSharp.HttpUserAgentParser/Providers/HttpUserAgentParserDefaultProvider.cs @@ -0,0 +1,10 @@ +// Copyright © myCSharp 2020-2021, all rights reserved + +namespace MyCSharp.HttpUserAgentParser.Providers +{ + public class HttpUserAgentParserDefaultProvider : IHttpUserAgentParserProvider + { + public HttpUserAgentInformation Parse(string userAgent) + => HttpUserAgentParser.Parse(userAgent); + } +} \ No newline at end of file diff --git a/src/MyCSharp.HttpUserAgentParser/Providers/IHttpUserAgentParserProvider.cs b/src/MyCSharp.HttpUserAgentParser/Providers/IHttpUserAgentParserProvider.cs new file mode 100644 index 0000000..f0eb9a9 --- /dev/null +++ b/src/MyCSharp.HttpUserAgentParser/Providers/IHttpUserAgentParserProvider.cs @@ -0,0 +1,9 @@ +// Copyright © myCSharp 2020-2021, all rights reserved + +namespace MyCSharp.HttpUserAgentParser.Providers +{ + public interface IHttpUserAgentParserProvider + { + HttpUserAgentInformation Parse(string userAgent); + } +} \ No newline at end of file diff --git a/tests/MyCSharp.HttpUserAgentParser.AspNetCore.UnitTests/DependencyInjection/HttpUserAgentParserServiceCollectionExtensionsTests.cs b/tests/MyCSharp.HttpUserAgentParser.AspNetCore.UnitTests/DependencyInjection/HttpUserAgentParserServiceCollectionExtensionsTests.cs new file mode 100644 index 0000000..f82442f --- /dev/null +++ b/tests/MyCSharp.HttpUserAgentParser.AspNetCore.UnitTests/DependencyInjection/HttpUserAgentParserServiceCollectionExtensionsTests.cs @@ -0,0 +1,28 @@ +// Copyright © myCSharp 2020-2021, all rights reserved + +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using MyCSharp.HttpUserAgentParser.AspNetCore.DependencyInjection; +using MyCSharp.HttpUserAgentParser.DependencyInjection; +using Xunit; + +namespace MyCSharp.HttpUserAgentParser.AspNetCore.UnitTests.DependencyInjection +{ + public class HttpUserAgentParserDependencyInjectionOptionsExtensionsTests + { + [Fact] + public void AddHttpUserAgentParserAccessor() + { + ServiceCollection services = new(); + HttpUserAgentParserDependencyInjectionOptions options = new(services); + + options.AddHttpUserAgentParserAccessor(); + + services.Count.Should().Be(1); + + services[0].ServiceType.Should().Be(); + services[0].ImplementationType.Should().Be(); + services[0].Lifetime.Should().Be(ServiceLifetime.Singleton); + } + } +} diff --git a/tests/MyCSharp.HttpUserAgentParser.AspNetCore.UnitTests/HttpUserAgentParserAccessorTests.cs b/tests/MyCSharp.HttpUserAgentParser.AspNetCore.UnitTests/HttpUserAgentParserAccessorTests.cs new file mode 100644 index 0000000..306e1ad --- /dev/null +++ b/tests/MyCSharp.HttpUserAgentParser.AspNetCore.UnitTests/HttpUserAgentParserAccessorTests.cs @@ -0,0 +1,37 @@ +// Copyright © myCSharp 2020-2021, all rights reserved + +using FluentAssertions; +using Microsoft.AspNetCore.Http; +using Moq; +using MyCSharp.HttpUserAgentParser.Providers; +using Xunit; + +namespace MyCSharp.HttpUserAgentParser.AspNetCore.UnitTests +{ + public class HttpUserAgentParserAccessorTests + { + [Theory] + [InlineData("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36 Edg/90.0.818.62")] + public void Get(string userAgent) + { + HttpUserAgentInformation userAgentInformation = HttpUserAgentInformation.Parse(userAgent); + + Mock httpMock = new(); + { + DefaultHttpContext context = new DefaultHttpContext(); + context.Request.Headers["User-Agent"] = userAgent; + httpMock.Setup(_ => _.HttpContext).Returns(context); + } + Mock parserMock = new(); + { + parserMock.Setup(x => x.Parse(userAgent)).Returns(userAgentInformation); + } + + HttpUserAgentParserAccessor accessor = new HttpUserAgentParserAccessor(httpMock.Object, parserMock.Object); + HttpUserAgentInformation info = accessor.Get(); + + info.Should().Be(userAgentInformation); + parserMock.Verify(x => x.Parse(userAgent), Times.Once); + } + } +} diff --git a/tests/MyCSharp.HttpUserAgentParser.AspNetCore.UnitTests/MyCSharp.HttpUserAgentParser.AspNetCore.UnitTests.csproj b/tests/MyCSharp.HttpUserAgentParser.AspNetCore.UnitTests/MyCSharp.HttpUserAgentParser.AspNetCore.UnitTests.csproj new file mode 100644 index 0000000..3edaabe --- /dev/null +++ b/tests/MyCSharp.HttpUserAgentParser.AspNetCore.UnitTests/MyCSharp.HttpUserAgentParser.AspNetCore.UnitTests.csproj @@ -0,0 +1,28 @@ + + + + Exe + net5.0 + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + \ No newline at end of file diff --git a/tests/MyCSharp.HttpUserAgentParser.MemoryCache.UnitTests/DependencyInjection/HttpUserAgentParserMemoryCacheServiceCollectionExtensionssTests.cs b/tests/MyCSharp.HttpUserAgentParser.MemoryCache.UnitTests/DependencyInjection/HttpUserAgentParserMemoryCacheServiceCollectionExtensionssTests.cs new file mode 100644 index 0000000..f3604a3 --- /dev/null +++ b/tests/MyCSharp.HttpUserAgentParser.MemoryCache.UnitTests/DependencyInjection/HttpUserAgentParserMemoryCacheServiceCollectionExtensionssTests.cs @@ -0,0 +1,30 @@ +// Copyright © myCSharp 2020-2021, all rights reserved + +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using MyCSharp.HttpUserAgentParser.MemoryCache.DependencyInjection; +using MyCSharp.HttpUserAgentParser.Providers; +using Xunit; + +namespace MyCSharp.HttpUserAgentParser.MemoryCache.UnitTests.DependencyInjection +{ + public class HttpUserAgentParserMemoryCacheServiceCollectionExtensionssTests + { + [Fact] + public void AddHttpUserAgentMemoryCachedParser() + { + ServiceCollection services = new(); + + services.AddHttpUserAgentMemoryCachedParser(); + + services.Count.Should().Be(2); + + services[0].ImplementationInstance.Should().BeOfType(); + services[0].Lifetime.Should().Be(ServiceLifetime.Singleton); + + services[1].ServiceType.Should().Be(); + services[1].ImplementationType.Should().Be(); + services[1].Lifetime.Should().Be(ServiceLifetime.Singleton); + } + } +} diff --git a/tests/MyCSharp.HttpUserAgentParser.MemoryCache.UnitTests/HttpUserAgentParserMemoryCachedProviderOptionsTests.cs b/tests/MyCSharp.HttpUserAgentParser.MemoryCache.UnitTests/HttpUserAgentParserMemoryCachedProviderOptionsTests.cs new file mode 100644 index 0000000..ac84bb4 --- /dev/null +++ b/tests/MyCSharp.HttpUserAgentParser.MemoryCache.UnitTests/HttpUserAgentParserMemoryCachedProviderOptionsTests.cs @@ -0,0 +1,54 @@ +// Copyright © myCSharp 2020-2021, all rights reserved + +using FluentAssertions; +using Microsoft.Extensions.Caching.Memory; +using Xunit; + +namespace MyCSharp.HttpUserAgentParser.MemoryCache.UnitTests +{ + public class HttpUserAgentParserMemoryCachedProviderOptionsTests + { + [Fact] + public void Ctor() + { + MemoryCacheOptions cacheOptions = new(); + MemoryCacheEntryOptions cacheEntryOptions = new(); + + HttpUserAgentParserMemoryCachedProviderOptions options = new(cacheOptions, cacheEntryOptions); + + options.CacheOptions.Should().Be(cacheOptions); + options.CacheEntryOptions.Should().Be(cacheEntryOptions); + } + + [Fact] + public void Ctor_MemoryCacheOptions() + { + MemoryCacheOptions cacheOptions = new(); + + HttpUserAgentParserMemoryCachedProviderOptions options = new(cacheOptions); + + options.CacheOptions.Should().Be(cacheOptions); + options.CacheEntryOptions.Should().NotBeNull(); + } + + [Fact] + public void Ctor_MemoryCacheEntryOptions() + { + MemoryCacheEntryOptions cacheEntryOptions = new(); + + HttpUserAgentParserMemoryCachedProviderOptions options = new(cacheEntryOptions); + + options.CacheOptions.Should().NotBeNull(); + options.CacheEntryOptions.Should().Be(cacheEntryOptions); + } + + [Fact] + public void Ctor_Empty() + { + HttpUserAgentParserMemoryCachedProviderOptions options = new(); + + options.CacheOptions.Should().NotBeNull(); + options.CacheEntryOptions.Should().NotBeNull(); + } + } +} diff --git a/tests/MyCSharp.HttpUserAgentParser.MemoryCache.UnitTests/HttpUserAgentParserMemoryCachedProviderTests.cs b/tests/MyCSharp.HttpUserAgentParser.MemoryCache.UnitTests/HttpUserAgentParserMemoryCachedProviderTests.cs new file mode 100644 index 0000000..53d5c58 --- /dev/null +++ b/tests/MyCSharp.HttpUserAgentParser.MemoryCache.UnitTests/HttpUserAgentParserMemoryCachedProviderTests.cs @@ -0,0 +1,45 @@ +// Copyright © myCSharp 2020-2021, all rights reserved + +using FluentAssertions; +using Microsoft.Extensions.Caching.Memory; +using Xunit; + +namespace MyCSharp.HttpUserAgentParser.MemoryCache.UnitTests +{ + public class HttpUserAgentParserMemoryCachedProviderTests + { + [Fact] + public void Parse() + { + HttpUserAgentParserMemoryCachedProviderOptions cachedProviderOptions = new(); + IMemoryCache memoryCache = new Microsoft.Extensions.Caching.Memory.MemoryCache(cachedProviderOptions.CacheOptions); + + HttpUserAgentParserMemoryCachedProvider provider = new(memoryCache, cachedProviderOptions); + + // create first + string userAgentOne = + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36 Edg/90.0.818.62"; + + HttpUserAgentInformation infoOne = provider.Parse(userAgentOne); + + infoOne.Name.Should().Be("Edge"); + infoOne.Version.Should().Be("90.0.818.62"); + + // check duplicate + + HttpUserAgentInformation infoDuplicate = provider.Parse(userAgentOne); + + infoDuplicate.Name.Should().Be("Edge"); + infoDuplicate.Version.Should().Be("90.0.818.62"); + + // create second + + string userAgentTwo = "Mozilla/5.0 (Android 4.4; Tablet; rv:41.0) Gecko/41.0 Firefox/41.0"; + + HttpUserAgentInformation infoTwo = provider.Parse(userAgentTwo); + + infoTwo.Name.Should().Be("Firefox"); + infoTwo.Version.Should().Be("41.0"); + } + } +} diff --git a/tests/MyCSharp.HttpUserAgentParser.MemoryCache.UnitTests/MyCSharp.HttpUserAgentParser.MemoryCache.UnitTests.csproj b/tests/MyCSharp.HttpUserAgentParser.MemoryCache.UnitTests/MyCSharp.HttpUserAgentParser.MemoryCache.UnitTests.csproj new file mode 100644 index 0000000..75ba3b0 --- /dev/null +++ b/tests/MyCSharp.HttpUserAgentParser.MemoryCache.UnitTests/MyCSharp.HttpUserAgentParser.MemoryCache.UnitTests.csproj @@ -0,0 +1,29 @@ + + + + Exe + net5.0 + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + \ No newline at end of file diff --git a/tests/MyCSharp.HttpUserAgentParser.UnitTests/DependencyInjection/HttpUserAgentParserDependencyInjectionOptions.cs b/tests/MyCSharp.HttpUserAgentParser.UnitTests/DependencyInjection/HttpUserAgentParserDependencyInjectionOptions.cs new file mode 100644 index 0000000..81437a2 --- /dev/null +++ b/tests/MyCSharp.HttpUserAgentParser.UnitTests/DependencyInjection/HttpUserAgentParserDependencyInjectionOptions.cs @@ -0,0 +1,23 @@ +// Copyright © myCSharp 2020-2021, all rights reserved + +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using Moq; +using MyCSharp.HttpUserAgentParser.DependencyInjection; +using Xunit; + +namespace MyCSharp.HttpUserAgentParser.UnitTests.DependencyInjection +{ + public class UserAgentParserDependencyInjectionOptionsTests + { + [Fact] + public void Ctor_Should_Set_Property() + { + Mock scMock = new(); + + HttpUserAgentParserDependencyInjectionOptions options = new(scMock.Object); + + options.Services.Should().BeEquivalentTo(scMock.Object); + } + } +} diff --git a/tests/MyCSharp.HttpUserAgentParser.UnitTests/DependencyInjection/HttpUserAgentParserServiceCollectionExtensionsTests.cs b/tests/MyCSharp.HttpUserAgentParser.UnitTests/DependencyInjection/HttpUserAgentParserServiceCollectionExtensionsTests.cs new file mode 100644 index 0000000..e456ca1 --- /dev/null +++ b/tests/MyCSharp.HttpUserAgentParser.UnitTests/DependencyInjection/HttpUserAgentParserServiceCollectionExtensionsTests.cs @@ -0,0 +1,57 @@ +// Copyright © myCSharp 2020-2021, all rights reserved + +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using MyCSharp.HttpUserAgentParser.DependencyInjection; +using MyCSharp.HttpUserAgentParser.Providers; +using Xunit; + +namespace MyCSharp.HttpUserAgentParser.UnitTests.DependencyInjection +{ + public class HttpUserAgentParserMemoryCacheServiceCollectionExtensions + { + public class TestHttpUserAgentParserProvider : IHttpUserAgentParserProvider + { + public HttpUserAgentInformation Parse(string userAgent) => throw new System.NotImplementedException(); + } + + [Fact] + public void AddHttpUserAgentParser() + { + ServiceCollection services = new(); + + services.AddHttpUserAgentParser(); + + services.Count.Should().Be(1); + services[0].ServiceType.Should().Be(); + services[0].ImplementationType.Should().Be(); + services[0].Lifetime.Should().Be(ServiceLifetime.Singleton); + } + + [Fact] + public void AddHttpUserAgentCachedParser() + { + ServiceCollection services = new(); + + services.AddHttpUserAgentCachedParser(); + + services.Count.Should().Be(1); + services[0].ServiceType.Should().Be(); + services[0].ImplementationType.Should().Be(); + services[0].Lifetime.Should().Be(ServiceLifetime.Singleton); + } + + [Fact] + public void AddHttpUserAgentParser_With_Generic() + { + ServiceCollection services = new(); + + services.AddHttpUserAgentParser(); + + services.Count.Should().Be(1); + services[0].ServiceType.Should().Be(); + services[0].ImplementationType.Should().Be(); + services[0].Lifetime.Should().Be(ServiceLifetime.Singleton); + } + } +} diff --git a/tests/MyCSharp.HttpUserAgentParser.UnitTests/HttpUserAgentInformationExtensionsTests.cs b/tests/MyCSharp.HttpUserAgentParser.UnitTests/HttpUserAgentInformationExtensionsTests.cs new file mode 100644 index 0000000..5d50374 --- /dev/null +++ b/tests/MyCSharp.HttpUserAgentParser.UnitTests/HttpUserAgentInformationExtensionsTests.cs @@ -0,0 +1,51 @@ +// Copyright © myCSharp 2020-2021, all rights reserved + +using FluentAssertions; +using Xunit; + +namespace MyCSharp.HttpUserAgentParser.UnitTests +{ + public class HttpUserAgentInformationExtensionsTests + { + [Theory] + [InlineData("Mozilla/5.0 (Linux; Android 10; HD1913) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.210 Mobile Safari/537.36 EdgA/46.3.4.5155", HttpUserAgentType.Browser, true)] + [InlineData("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36 Edg/90.0.818.62", HttpUserAgentType.Browser, false)] + [InlineData("Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML,like Gecko) Version/9.0 Mobile/13B143 Safari/601.1 (compatible; AdsBot-Google-Mobile; +http://www.google.com/mobile/adsbot.html)", HttpUserAgentType.Robot, false)] + [InlineData("APIs-Google (+https://developers.google.com/webmasters/APIs-Google.html)", HttpUserAgentType.Robot, false)] + [InlineData("Invalid user agent", HttpUserAgentType.Unknown, false)] + public void IsType(string userAgent, HttpUserAgentType expectedType, bool isMobile) + { + HttpUserAgentInformation info = HttpUserAgentInformation.Parse(userAgent); + + if (expectedType == HttpUserAgentType.Browser) + { + info.IsType(HttpUserAgentType.Browser).Should().Be(true); + info.IsType(HttpUserAgentType.Robot).Should().Be(false); + info.IsType(HttpUserAgentType.Unknown).Should().Be(false); + + info.IsBrowser().Should().Be(true); + info.IsRobot().Should().Be(false); + } + else if (expectedType == HttpUserAgentType.Robot) + { + info.IsType(HttpUserAgentType.Browser).Should().Be(false); + info.IsType(HttpUserAgentType.Robot).Should().Be(true); + info.IsType(HttpUserAgentType.Unknown).Should().Be(false); + + info.IsBrowser().Should().Be(false); + info.IsRobot().Should().Be(true); + } + else if (expectedType == HttpUserAgentType.Unknown) + { + info.IsType(HttpUserAgentType.Browser).Should().Be(false); + info.IsType(HttpUserAgentType.Robot).Should().Be(false); + info.IsType(HttpUserAgentType.Unknown).Should().Be(true); + + info.IsBrowser().Should().Be(false); + info.IsRobot().Should().Be(false); + } + + info.IsMobile().Should().Be(isMobile); + } + } +} diff --git a/tests/MyCSharp.HttpUserAgentParser.UnitTests/HttpUserAgentInformationTests.cs b/tests/MyCSharp.HttpUserAgentParser.UnitTests/HttpUserAgentInformationTests.cs new file mode 100644 index 0000000..171d973 --- /dev/null +++ b/tests/MyCSharp.HttpUserAgentParser.UnitTests/HttpUserAgentInformationTests.cs @@ -0,0 +1,72 @@ +// Copyright © myCSharp 2020-2021, all rights reserved + +using System.Text.RegularExpressions; +using FluentAssertions; +using Xunit; + +namespace MyCSharp.HttpUserAgentParser.UnitTests +{ + public class HttpUserAgentInformationTests + { + [Theory] + [InlineData("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36 Edg/90.0.818.62")] + public void Parse(string userAgent) + { + HttpUserAgentInformation ua1 = HttpUserAgentParser.Parse(userAgent); + HttpUserAgentInformation ua2 = HttpUserAgentInformation.Parse(userAgent); + + ua1.Should().BeEquivalentTo(ua2); + } + + [Theory] + [InlineData("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36 Edg/90.0.818.62")] + public void CreateForRobot(string userAgent) + { + + HttpUserAgentInformation ua = HttpUserAgentInformation.CreateForRobot(userAgent, "Chrome"); + + ua.UserAgent.Should().Be(userAgent); + ua.Type.Should().Be(HttpUserAgentType.Robot); + ua.Platform.Should().Be(null); + ua.Name.Should().Be("Chrome"); + ua.Version.Should().Be(null); + ua.MobileDeviceType.Should().Be(null); + } + + [Theory] + [InlineData("Mozilla/5.0 (Linux; Android 10; HD1913) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.210 Mobile Safari/537.36 EdgA/46.3.4.5155")] + public void CreateForBrowser(string userAgent) + { + HttpUserAgentPlatformInformation platformInformation = + new HttpUserAgentPlatformInformation(new Regex(""), "Android", HttpUserAgentPlatformType.Android); + + HttpUserAgentInformation ua = HttpUserAgentInformation.CreateForBrowser(userAgent, + platformInformation, "Edge", "46.3.4.5155", "Android"); + + ua.UserAgent.Should().Be(userAgent); + ua.Type.Should().Be(HttpUserAgentType.Browser); + ua.Platform.Should().Be(platformInformation); + ua.Name.Should().Be("Edge"); + ua.Version.Should().Be("46.3.4.5155"); + ua.MobileDeviceType.Should().Be("Android"); + } + + [Theory] + [InlineData("Invalid user agent")] + public void CreateForUnknown(string userAgent) + { + HttpUserAgentPlatformInformation platformInformation = + new(new Regex(""), "Batman", HttpUserAgentPlatformType.Linux); + + HttpUserAgentInformation ua = + HttpUserAgentInformation.CreateForUnknown(userAgent, platformInformation, null); + + ua.UserAgent.Should().Be(userAgent); + ua.Type.Should().Be(HttpUserAgentType.Unknown); + ua.Platform.Should().Be(platformInformation); + ua.Name.Should().Be(null); + ua.Version.Should().Be(null); + ua.MobileDeviceType.Should().Be(null); + } + } +} diff --git a/tests/MyCSharp.HttpUserAgentParser.UnitTests/HttpUserAgentParserTests.cs b/tests/MyCSharp.HttpUserAgentParser.UnitTests/HttpUserAgentParserTests.cs new file mode 100644 index 0000000..517cd5d --- /dev/null +++ b/tests/MyCSharp.HttpUserAgentParser.UnitTests/HttpUserAgentParserTests.cs @@ -0,0 +1,152 @@ +// Copyright © myCSharp 2020-2021, all rights reserved + +using FluentAssertions; +using Xunit; + +namespace MyCSharp.HttpUserAgentParser.UnitTests +{ + public class HttpUserAgentParserTests + { + [Theory] + // IE + [InlineData("Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0; WOW64; Trident/4.0;)", "Internet Explorer", "7.0", "Windows Vista", HttpUserAgentPlatformType.Windows, null)] + [InlineData("Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0)", "Internet Explorer", "8.0", "Windows XP", HttpUserAgentPlatformType.Windows, null)] + [InlineData("Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; Trident/4.0)", "Internet Explorer", "8.0", "Windows 7", HttpUserAgentPlatformType.Windows, null)] + [InlineData("Mozilla/4.0 (compatible; MSIE 9.0; Windows NT 6.0)", "Internet Explorer", "9.0", "Windows Vista", HttpUserAgentPlatformType.Windows, null)] + [InlineData("Mozilla/4.0 (compatible; MSIE 9.0; Windows NT 6.1)", "Internet Explorer", "9.0", "Windows 7", HttpUserAgentPlatformType.Windows, null)] + [InlineData("Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; WOW64; Trident/6.0)", "Internet Explorer", "10.0", "Windows 7", HttpUserAgentPlatformType.Windows, null)] + [InlineData("Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.2)", "Internet Explorer", "10.0", "Windows 8", HttpUserAgentPlatformType.Windows, null)] + [InlineData("Mozilla/5.0 (Windows NT 6.1; Trident/7.0; rv:11.0) like Gecko", "Internet Explorer", "11.0", "Windows 7", HttpUserAgentPlatformType.Windows, null)] + [InlineData("Mozilla/5.0 (Windows NT 6.2; Trident/7.0; rv:11.0) like Gecko", "Internet Explorer", "11.0", "Windows 8", HttpUserAgentPlatformType.Windows, null)] + [InlineData("Mozilla/5.0 (Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko", "Internet Explorer", "11.0", "Windows 8.1", HttpUserAgentPlatformType.Windows, null)] + [InlineData("Mozilla/5.0 (Windows NT 10.0; Trident/7.0; rv:11.0) like Gecko", "Internet Explorer", "11.0", "Windows 10", HttpUserAgentPlatformType.Windows, null)] + // Chrome + [InlineData("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36", "Chrome", "90.0.4430.212", "Windows 10", HttpUserAgentPlatformType.Windows, null)] + [InlineData("Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36", "Chrome", "90.0.4430.212", "Windows 10", HttpUserAgentPlatformType.Windows, null)] + [InlineData("Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36", "Chrome", "90.0.4430.212", "Windows 10", HttpUserAgentPlatformType.Windows, null)] + [InlineData("Mozilla/5.0 (Macintosh; Intel Mac OS X 11_3_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36", "Chrome", "90.0.4430.212", "Mac OS X", HttpUserAgentPlatformType.MacOS, null)] + [InlineData("Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36", "Chrome", "90.0.4430.212", "Linux", HttpUserAgentPlatformType.Linux, null)] + [InlineData("Mozilla/5.0 (iPhone; CPU iPhone OS 14_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/90.0.4430.78 Mobile/15E148 Safari/604.1", "Chrome", "90.0.4430.78", "iOS", HttpUserAgentPlatformType.IOS, "Apple iPhone")] + [InlineData("Mozilla/5.0 (iPad; CPU OS 14_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/90.0.4430.78 Mobile/15E148 Safari/604.1", "Chrome", "90.0.4430.78", "iOS", HttpUserAgentPlatformType.IOS, "Apple iPad")] + [InlineData("Mozilla/5.0 (iPod; CPU iPhone OS 14_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/90.0.4430.78 Mobile/15E148 Safari/604.1", "Chrome", "90.0.4430.78", "iOS", HttpUserAgentPlatformType.IOS, "Apple iPod")] + [InlineData("Mozilla/5.0 (Linux; Android 10) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.210 Mobile Safari/537.36", "Chrome", "90.0.4430.210", "Android", HttpUserAgentPlatformType.Android, "Android")] + [InlineData("Mozilla/5.0 (Linux; Android 10; SM-A205U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.210 Mobile Safari/537.36", "Chrome", "90.0.4430.210", "Android", HttpUserAgentPlatformType.Android, "Android")] + [InlineData("Mozilla/5.0 (Linux; Android 10; LM-Q720) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.210 Mobile Safari/537.36", "Chrome", "90.0.4430.210", "Android", HttpUserAgentPlatformType.Android, "Android")] + // Safari + [InlineData("Mozilla/5.0 (Windows; U; Windows NT 10.0; en-US) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/11.0 Safari/605.1.15", "Safari", "605.1.15", "Windows 10", HttpUserAgentPlatformType.Windows, null)] + [InlineData("Mozilla/5.0 (Macintosh; Intel Mac OS X 11_3_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1 Safari/605.1.15", "Safari", "605.1.15", "Mac OS X", HttpUserAgentPlatformType.MacOS, null)] + [InlineData("Mozilla/5.0 (iPhone; CPU iPhone OS 14_5_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Mobile/15E148 Safari/604.1", "Safari", "604.1", "iOS", HttpUserAgentPlatformType.IOS, "Apple iPhone")] + [InlineData("Mozilla/5.0 (iPod touch; CPU iPhone 14_5_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Mobile/15E148 Safari/604.1", "Safari", "604.1", "iOS", HttpUserAgentPlatformType.IOS, "Apple iPod")] + // Edge + [InlineData("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36 Edg/90.0.818.51", "Edge", "90.0.818.51", "Windows 10", HttpUserAgentPlatformType.Windows, null)] + [InlineData("Mozilla/5.0 (Macintosh; Intel Mac OS X 11_3_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36 Edg/90.0.818.51", "Edge", "90.0.818.51", "Mac OS X", HttpUserAgentPlatformType.MacOS, null)] + [InlineData("Mozilla/5.0 (Linux; Android 10; HD1913) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.210 Mobile Safari/537.36 EdgA/46.3.4.5155", "Edge", "46.3.4.5155", "Android", HttpUserAgentPlatformType.Android, "Android")] + [InlineData("Mozilla/5.0 (iPhone; CPU iPhone OS 14_5_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 EdgiOS/46.3.13 Mobile/15E148 Safari/605.1.15", "Edge", "46.3.13", "iOS", HttpUserAgentPlatformType.IOS, "Apple iPhone")] + [InlineData("Mozilla/5.0 (Windows NT 10.0; Win64; x64; Xbox; Xbox One) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36 Edge/44.18363.8131", "Edge", "44.18363.8131", "Windows 10", HttpUserAgentPlatformType.Windows, null)] + // Firefox + [InlineData("Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:88.0) Gecko/20100101 Firefox/88.0", "Firefox", "88.0", "Windows 10", HttpUserAgentPlatformType.Windows, null)] + [InlineData("Mozilla/5.0 (Macintosh; Intel Mac OS X 11.3; rv:88.0) Gecko/20100101 Firefox/88.0", "Firefox", "88.0", "Mac OS X", HttpUserAgentPlatformType.MacOS, null)] + [InlineData("Mozilla/5.0 (X11; Linux i686; rv:88.0) Gecko/20100101 Firefox/88.0", "Firefox", "88.0", "Linux", HttpUserAgentPlatformType.Linux, null)] + [InlineData("Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:88.0) Gecko/20100101 Firefox/88.0", "Firefox", "88.0", "Linux", HttpUserAgentPlatformType.Linux, null)] + [InlineData("Mozilla/5.0 (iPhone; CPU iPhone OS 11_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) FxiOS/33.0 Mobile/15E148 Safari/605.1.15", "Firefox", "33.0", "iOS", HttpUserAgentPlatformType.IOS, "Apple iPhone")] + [InlineData("Mozilla/5.0 (Android 11; Mobile; rv:68.0) Gecko/68.0 Firefox/88.0", "Firefox", "88.0", "Android", HttpUserAgentPlatformType.Android, "Android")] + // Opera + [InlineData("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36 OPR/76.0.4017.107", "Opera", "76.0.4017.107", "Windows 10", HttpUserAgentPlatformType.Windows, null)] + [InlineData("Mozilla/5.0 (Macintosh; Intel Mac OS X 11_3_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36 OPR/76.0.4017.107", "Opera", "76.0.4017.107", "Mac OS X", HttpUserAgentPlatformType.MacOS, null)] + [InlineData("Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36 OPR/76.0.4017.107", "Opera", "76.0.4017.107", "Linux", HttpUserAgentPlatformType.Linux, null)] + [InlineData("Mozilla/5.0 (Linux; Android 10; VOG-L29) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.210 Mobile Safari/537.36 OPR/63.0.3216.58473", "Opera", "63.0.3216.58473", "Android", HttpUserAgentPlatformType.Android, "Android")] + public void BrowserTests(string ua, string name, string version, string platformName, HttpUserAgentPlatformType platformType, string mobileDeviceType) + { + HttpUserAgentInformation uaInfo = HttpUserAgentInformation.Parse(ua); + + uaInfo.Name.Should().Be(name); + uaInfo.Version.Should().Be(version); + uaInfo.UserAgent.Should().Be(ua); + + uaInfo.Type.Should().Be(HttpUserAgentType.Browser); + + HttpUserAgentPlatformInformation platform = uaInfo.Platform.GetValueOrDefault(); + platform.PlatformType.Should().Be(platformType); + platform.Name.Should().Be(platformName); + + uaInfo.MobileDeviceType.Should().Be(mobileDeviceType); + + uaInfo.IsBrowser().Should().Be(true); + uaInfo.IsMobile().Should().Be(mobileDeviceType is not null); + uaInfo.IsRobot().Should().Be(false); + } + + [Theory] + // Google https://developers.google.com/search/docs/advanced/crawling/overview-google-crawlers + [InlineData("APIs-Google (+https://developers.google.com/webmasters/APIs-Google.html)", "APIs Google")] + [InlineData("Mediapartners-Google", "Mediapartners Google")] + [InlineData("Mozilla/5.0 (Linux; Android 5.0; SM-G920A) AppleWebKit (KHTML, like Gecko) Chrome Mobile Safari (compatible; AdsBot-Google-Mobile; +http://www.google.com/mobile/adsbot.html)", "AdsBot Google Mobile")] + [InlineData("Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML,like Gecko) Version/9.0 Mobile/13B143 Safari/601.1 (compatible; AdsBot-Google-Mobile; +http://www.google.com/mobile/adsbot.html)", "AdsBot Google Mobile")] + [InlineData("AdsBot-Google (+http://www.google.com/adsbot.html)", "AdsBot Google")] + [InlineData("Googlebot-Image/1.0", "Googlebot")] + [InlineData("Googlebot-News", "Googlebot")] + [InlineData("Googlebot-Video/1.0", "Googlebot")] + [InlineData("Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)", "Googlebot")] + [InlineData("Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; Googlebot/2.1; +http://www.google.com/bot.html) Chrome/1.2.3 Safari/537.36", "Googlebot")] + [InlineData("Googlebot/2.1 (+http://www.google.com/bot.html)", "Googlebot")] + [InlineData("Mozilla/5.0 (Linux; Android 6.0.1; Nexus 5X Build/MMB29P) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/1.2.3 Mobile Safari/537.36 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)", "Googlebot")] + [InlineData("Mediapartners-Google/2.1; +http://www.google.com/bot.html)", "Mediapartners Google")] + [InlineData("FeedFetcher-Google; (+http://www.google.com/feedfetcher.html)", "FeedFetcher-Google")] + [InlineData("Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.118 Safari/537.36 (compatible; Google-Read-Aloud; +https://developers.google.com/search/docs/advanced/crawling/overview-google-crawlers)", "Google-Read-Aloud")] + [InlineData("Mozilla/5.0 (Linux; Android 7.0; SM-G930V Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.125 Mobile Safari/537.36 (compatible; Google-Read-Aloud; +https://developers.google.com/search/docs/advanced/crawling/overview-google-crawlers)", "Google-Read-Aloud")] + [InlineData("Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012; DuplexWeb-Google/1.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.131 Mobile Safari/537.36", "DuplexWeb-Google")] + [InlineData("Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.75 Safari/537.36 Google Favicon", "Google Favicon")] + [InlineData("Mozilla/5.0 (Linux; Android 4.2.1; en-us; Nexus 5 Build/JOP40D) AppleWebKit/535.19 (KHTML, like Gecko; googleweblight) Chrome/38.0.1025.166 Mobile Safari/535.19", "Google Web Light")] + [InlineData("Mozilla/5.0 (X11; Linux x86_64; Storebot-Google/1.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36", "Storebot-Google")] + [InlineData("Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012; Storebot-Google/1.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Mobile Safari/537.36", "Storebot-Google")] + // Bing + [InlineData("Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)", "BingBot")] + [InlineData("Mozilla/5.0 (iPhone; CPU iPhone OS 7_0 like Mac OS X) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11A465 Safari/9537.53 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)", "BingBot")] + [InlineData("Mozilla/5.0 (Windows Phone 8.1; ARM; Trident/7.0; Touch; rv:11.0; IEMobile/11.0; NOKIA; Lumia 530) like Gecko (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)", "BingBot")] + [InlineData("Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm) Chrome/1.2.3.4 Safari/537.36 Edg/1.2.3.4", "BingBot")] + [InlineData("Mozilla/5.0 (Linux; Android 6.0.1; Nexus 5X Build/MMB29P) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/1.2.3.4  Mobile Safari/537.36 Edg/1.2.3.4 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)", "BingBot")] + [InlineData("Mozilla/5.0 (compatible; Baiduspider/2.0; +http://www.baidu.com/search/spider.html)", "Baiduspider")] + [InlineData("Mozilla/5.0 (compatible; MJ12bot/v1.4.5; http://www.majestic12.co.uk/bot.php?+)", "Majestic")] + [InlineData("Mozilla/5.0 (compatible; Yahoo! Slurp; http://help.yahoo.com/help/us/ysearch/slurp)", "Slurp")] + [InlineData("Mozilla/5.0 (compatible; MegaIndex.ru/2.0; +http://megaindex.com/crawler)", "MegaIndex")] + [InlineData("Mozilla/5.0 (compatible; AhrefsBot/5.2; +http://ahrefs.com/robot/)", "Ahrefs")] + [InlineData("Mozilla/5.0 (compatible; SemrushBot/7~bl; +http://www.semrush.com/bot.html)", "SEMRush")] + [InlineData("Mozilla/5.0 (compatible; DotBot/1.1; http://www.opensiteexplorer.org/dotbot, help@moz.com)", "OpenSite")] + [InlineData("Mozilla/5.0 (X11; U; Linux Core i7-4980HQ; de; rv:32.0; compatible; JobboerseBot; http://www.jobboerse.com/bot.htm) Gecko/20100101 Firefox/38.0", "Jobboerse")] + [InlineData("Mozilla/5.0 (compatible; MJ12bot/v1.4.8; http://mj12bot.com/)", "Majestic")] + [InlineData("Mozilla/5.0 (compatible; SemrushBot/2~bl; +http://www.semrush.com/bot.html)", "SEMRush")] + [InlineData("Mozilla/5.0 (compatible; YandexBot/3.0; +http://yandex.com/bots)", "YandexBot")] + [InlineData("Mozilla/5.0 (compatible; YandexImages/3.0; +http://yandex.com/bots)", "YandexImages")] + [InlineData("Mozilla/5.0 (compatible; Yahoo! Slurp/3.0; http://help.yahoo.com/help/us/ysearch/slurp)", "Slurp")] + [InlineData("msnbot/1.0 (+http://search.msn.com/msnbot.htm)", "MSNBot")] + [InlineData("msnbot/2.0b (+http://search.msn.com/msnbot.htm)", "MSNBot")] + [InlineData("Mozilla/5.0 (compatible; AhrefsBot/5.0; +http://ahrefs.com/robot/)", "Ahrefs")] + [InlineData("Mozilla/5.0 (compatible; seoscanners.net/1; +spider@seoscanners.net)", "SEO Scanners")] + [InlineData("Mozilla/5.0 (compatible; SEOkicks-Robot; +http://www.seokicks.de/robot.html)", "SEOkicks")] + [InlineData("facebookexternalhit/1.1 (+http://www.facebook.com/externalhit_uatext.php)", "Facebook")] + [InlineData("Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/534+ (KHTML, like Gecko) BingPreview/1.0b", "Bing Preview")] + [InlineData("CheckMarkNetwork/1.0 (+http://www.checkmarknetwork.com/spider.html)", "CheckMark")] + [InlineData("Mozilla/5.0 (compatible; BLEXBot/1.0; +http://webmeup-crawler.com/)", "BLEXBot")] + [InlineData("Mozilla/5.0 (compatible; Linux x86_64; Mail.RU_Bot/Fast/2.0; +http://go.mail.ru/help/robots)", "Mail.ru")] + [InlineData("Mozilla/5.0 (compatible; adscanner/)", "AdScanner")] + [InlineData("Mozilla/5.0 (compatible; SISTRIX Crawler; http://crawler.sistrix.net/)", "Sistrix")] + [InlineData("Mozilla/5.0 (Linux; Android 7.0;) AppleWebKit/537.36 (KHTML, like Gecko) Mobile Safari/537.36 (compatible; PetalBot;+https://aspiegel.com/petalbot)", "PetalBot")] + public void BotTests(string ua, string name) + { + HttpUserAgentInformation uaInfo = HttpUserAgentInformation.Parse(ua); + + uaInfo.Name.Should().Be(name); + uaInfo.Version.Should().Be(null); + uaInfo.UserAgent.Should().Be(ua); + + uaInfo.Type.Should().Be(HttpUserAgentType.Robot); + + uaInfo.Platform.Should().Be(null); + uaInfo.MobileDeviceType.Should().Be(null); + + uaInfo.IsBrowser().Should().Be(false); + uaInfo.IsMobile().Should().Be(false); + uaInfo.IsRobot().Should().Be(true); + } + } +} diff --git a/tests/MyCSharp.HttpUserAgentParser.UnitTests/HttpUserAgentPlatformInformationTests.cs b/tests/MyCSharp.HttpUserAgentParser.UnitTests/HttpUserAgentPlatformInformationTests.cs new file mode 100644 index 0000000..d4b79d2 --- /dev/null +++ b/tests/MyCSharp.HttpUserAgentParser.UnitTests/HttpUserAgentPlatformInformationTests.cs @@ -0,0 +1,25 @@ +// Copyright © myCSharp 2020-2021, all rights reserved + +using System.Text.RegularExpressions; +using FluentAssertions; +using Xunit; + +namespace MyCSharp.HttpUserAgentParser.UnitTests +{ + public class HttpUserAgentPlatformInformationTests + { + [Theory] + [InlineData("Batman", HttpUserAgentPlatformType.Android)] + [InlineData("Robin", HttpUserAgentPlatformType.Windows)] + public void Ctor(string name, HttpUserAgentPlatformType platform) + { + Regex regex = new(""); + + HttpUserAgentPlatformInformation info = new(regex, name, platform); + + info.Regex.Should().Be(regex); + info.Name.Should().Be(name); + info.PlatformType.Should().Be(platform); + } + } +} diff --git a/tests/MyCSharp.HttpUserAgentParser.UnitTests/HttpUserAgentPlatformTypeTests.cs b/tests/MyCSharp.HttpUserAgentParser.UnitTests/HttpUserAgentPlatformTypeTests.cs new file mode 100644 index 0000000..5692d8a --- /dev/null +++ b/tests/MyCSharp.HttpUserAgentParser.UnitTests/HttpUserAgentPlatformTypeTests.cs @@ -0,0 +1,26 @@ +// Copyright © myCSharp 2020-2021, all rights reserved + +using FluentAssertions; +using Xunit; + +namespace MyCSharp.HttpUserAgentParser.UnitTests +{ + public class HttpUserAgentPlatformTypeTests + { + [Theory] + [InlineData(HttpUserAgentPlatformType.Unknown, 0)] + [InlineData(HttpUserAgentPlatformType.Generic, 1)] + [InlineData(HttpUserAgentPlatformType.Windows, 2)] + [InlineData(HttpUserAgentPlatformType.Linux, 3)] + [InlineData(HttpUserAgentPlatformType.Unix, 4)] + [InlineData(HttpUserAgentPlatformType.IOS, 5)] + [InlineData(HttpUserAgentPlatformType.MacOS, 6)] + [InlineData(HttpUserAgentPlatformType.BlackBerry, 7)] + [InlineData(HttpUserAgentPlatformType.Android, 8)] + [InlineData(HttpUserAgentPlatformType.Symbian, 9)] + public void TestValue(HttpUserAgentPlatformType type, byte value) + { + type.Should().BeEquivalentTo(value); + } + } +} diff --git a/tests/MyCSharp.HttpUserAgentParser.UnitTests/HttpUserAgentTypeTests.cs b/tests/MyCSharp.HttpUserAgentParser.UnitTests/HttpUserAgentTypeTests.cs new file mode 100644 index 0000000..f516e5f --- /dev/null +++ b/tests/MyCSharp.HttpUserAgentParser.UnitTests/HttpUserAgentTypeTests.cs @@ -0,0 +1,19 @@ +// Copyright © myCSharp 2020-2021, all rights reserved + +using FluentAssertions; +using Xunit; + +namespace MyCSharp.HttpUserAgentParser.UnitTests +{ + public class HttpUserAgentTypeTests + { + [Theory] + [InlineData(HttpUserAgentType.Unknown, 0)] + [InlineData(HttpUserAgentType.Browser, 1)] + [InlineData(HttpUserAgentType.Robot, 2)] + public void TestValue(HttpUserAgentType type, byte value) + { + type.Should().BeEquivalentTo(value); + } + } +} diff --git a/tests/MyCSharp.HttpUserAgentParser.UnitTests/MyCSharp.HttpUserAgentParser.UnitTests.csproj b/tests/MyCSharp.HttpUserAgentParser.UnitTests/MyCSharp.HttpUserAgentParser.UnitTests.csproj new file mode 100644 index 0000000..790d6e9 --- /dev/null +++ b/tests/MyCSharp.HttpUserAgentParser.UnitTests/MyCSharp.HttpUserAgentParser.UnitTests.csproj @@ -0,0 +1,28 @@ + + + + Exe + net5.0 + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + \ No newline at end of file diff --git a/tests/MyCSharp.HttpUserAgentParser.UnitTests/Providers/HttpUserAgentParserCachedProviderTests.cs b/tests/MyCSharp.HttpUserAgentParser.UnitTests/Providers/HttpUserAgentParserCachedProviderTests.cs new file mode 100644 index 0000000..43de1ec --- /dev/null +++ b/tests/MyCSharp.HttpUserAgentParser.UnitTests/Providers/HttpUserAgentParserCachedProviderTests.cs @@ -0,0 +1,53 @@ +// Copyright © myCSharp 2020-2021, all rights reserved + +using FluentAssertions; +using MyCSharp.HttpUserAgentParser.Providers; +using Xunit; + +namespace MyCSharp.HttpUserAgentParser.UnitTests.Providers +{ + public class HttpUserAgentParserCachedProviderTests + { + [Fact] + public void Parse() + { + HttpUserAgentParserCachedProvider provider = new HttpUserAgentParserCachedProvider(); + + provider.CacheEntryCount.Should().Be(0); + + // create first + string userAgentOne = + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36 Edg/90.0.818.62"; + + HttpUserAgentInformation infoOne = provider.Parse(userAgentOne); + + infoOne.Name.Should().Be("Edge"); + infoOne.Version.Should().Be("90.0.818.62"); + + provider.CacheEntryCount.Should().Be(1); + provider.HasCacheEntry(userAgentOne).Should().Be(true); + + // check duplicate + + HttpUserAgentInformation infoDuplicate = provider.Parse(userAgentOne); + + infoDuplicate.Name.Should().Be("Edge"); + infoDuplicate.Version.Should().Be("90.0.818.62"); + + provider.CacheEntryCount.Should().Be(1); + provider.HasCacheEntry(userAgentOne).Should().Be(true); + + // create second + + string userAgentTwo = "Mozilla/5.0 (Android 4.4; Tablet; rv:41.0) Gecko/41.0 Firefox/41.0"; + + HttpUserAgentInformation infoTwo = provider.Parse(userAgentTwo); + + infoTwo.Name.Should().Be("Firefox"); + infoTwo.Version.Should().Be("41.0"); + + provider.CacheEntryCount.Should().Be(2); + provider.HasCacheEntry(userAgentTwo).Should().Be(true); + } + } +} diff --git a/tests/MyCSharp.HttpUserAgentParser.UnitTests/Providers/HttpUserAgentParserDefaultProviderTests.cs b/tests/MyCSharp.HttpUserAgentParser.UnitTests/Providers/HttpUserAgentParserDefaultProviderTests.cs new file mode 100644 index 0000000..98b5a61 --- /dev/null +++ b/tests/MyCSharp.HttpUserAgentParser.UnitTests/Providers/HttpUserAgentParserDefaultProviderTests.cs @@ -0,0 +1,23 @@ +// Copyright © myCSharp 2020-2021, all rights reserved + +using FluentAssertions; +using MyCSharp.HttpUserAgentParser.Providers; +using Xunit; + +namespace MyCSharp.HttpUserAgentParser.UnitTests.Providers +{ + public class HttpUserAgentParserDefaultProviderTests + { + [Theory] + [InlineData("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36 Edg/90.0.818.62")] + public void Parse(string userAgent) + { + HttpUserAgentParserDefaultProvider provider = new(); + + HttpUserAgentInformation providerUserAgentInfo = provider.Parse(userAgent); + HttpUserAgentInformation userAgentInfo = HttpUserAgentInformation.Parse(userAgent); + + providerUserAgentInfo.Should().BeEquivalentTo(userAgentInfo); + } + } +} diff --git a/version.json b/version.json new file mode 100644 index 0000000..7ebe4ab --- /dev/null +++ b/version.json @@ -0,0 +1,19 @@ +{ + "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", + "version": "0.1", + "nugetPackageVersion": { + "semVer": 1 // optional. Set to either 1 or 2 to control how the NuGet package version string is generated. Default is 1. + }, + "publicReleaseRefSpec": [ + "^refs/heads/master$", // we release out of master + ], + "cloudBuild": { + "buildNumber": { + "enabled": true + } + }, + "release": { + "versionIncrement" : "build", + "firstUnstableTag": "preview" + } + } \ No newline at end of file